vlang / v

Simple, fast, safe, compiled language for developing maintainable software. Compiles itself in <1s with zero library dependencies. Supports automatic C => V translation. https://vlang.io
MIT License
35.81k stars 2.17k forks source link

Generics syntax discussion #2469

Closed medvednikov closed 2 years ago

medvednikov commented 5 years ago

Generics are back thanks to @shsr04.

This time they are more powerful (for example, multiple types are supported).

You can see some examples here: https://github.com/vlang/v/blob/master/vlib/compiler/tests/generic_test.v

One big change is the type parameters being removed from the function call:

foo<int>(1)
foo<string>('str')
foo<User>(user)

becomes

foo(1)
foo('str')
foo(user)

To me it looks a bit like overloaded functions, which are not allowed in V. Of course the function is still the same, and does the same thing, unlike overloaded functions which can do anything depending on the type.

Also some functionality is lost. vweb uses the following construct to build an application object:

// vweb.v
fn run<T>(port int) {
  mut app := T{}
  ...
}

All that's needed to set up a web application object:

fn main() {
  vweb.run<App>(port)
}

Now it has to be done with

fn main() {
  mut app := App{}
  vweb.run(mut app, port)
}

Which syntax do you prefer?

spytheman commented 5 years ago

The second syntax is easier to understand for me:

fn main() {
  mut app := App{}
  vweb.run(mut app, port)
}
Delta456 commented 5 years ago

Second syntax for me as well

Bowero commented 5 years ago

I agree with @spytheman and @Delta456 that the second syntax is more intuitive.

avitkauskas commented 5 years ago

I think this is not such a big problem. Second variant is much better for me. It's fine to call generic functions without the <T>, I think it's cleaner and it's usual in most languages suporting generics. Being strict is fine, but there are places where it makes sence to give some freedom.

But I have another remark. What about generic struct? Can we do that now? Is it needed?

struct Things<T> {
    a T
    b T
    c int
}

small := Things<int>{1, 1, 2}
big := Things<i64>{1, 1, 2}

// Or should it also then be?
small := Things{1, 1, 2}
big := Things{i64(1), i64(1), 2}

Like containers of different types in C++. Perhaps that comes from C++ to me, but I have a feeling that you have to indicate the type of the element when you create a general container, but you never bother specifying the exact type when calling the function (the type is inferred from the parameters). And I like it this way.

shsr04 commented 5 years ago

@avitkauskas Generic structs are not yet implemented, as you have probably found out by now.

For generic structs, it would be necessary to specify the type to allow partial initialization.

tobyjwebb commented 5 years ago

I think if it's a case of either/or, then the first syntax is a lot better. However, I do like the old way of calling vweb.run()... Could we make it optional? Best of both worlds?

medvednikov commented 5 years ago

The first syntax is the old syntax :) @tobyjwebb

tobyjwebb commented 5 years ago

:man_facepalming: Sorry, I meant the other way round... Not enough coffee yet.

medvednikov commented 5 years ago

@avitkauskas generic structs will be certainly supported. And yes, no way around specifying explicit types:

struct List<T>{}

list := List<int>{}
avitkauskas commented 5 years ago

This issue was opened primarily to discuss generic function call syntax. But we also have to discuss the generics type restriction syntax.

Today this compiles and crashes at run time yet:

fn div<T>(a, b T) T {
    return a / b
}
fn main() {
    div('aaa', 'bbb')
}

So, we need a syntax to restrict generic types to only certain sets of types, so that compiler could check that and though an error during compilation. Could it be one of these?

fn div<T {int, u32, i64, u64}>(a, b T) T { ... }
fn div<T [i64, u64], U [int]>(a T, b U) T { ... }

or perhaps even better

typeset Numbers {i8, byte, i16, u16, int, u32, i64, u64}
typeset UnsignedNumbers {byte, u16, u32, u64}
// with most usefull typesets predefined somewhere?
fn div<T Numbers, U UnsignedNumbers>(a T, b U) T { ... }

Or was it already thought about and decided?

medvednikov commented 5 years ago

It doesn't compile for me, I get a C error.

We just need to make V handle it:

error: invalid operands to binary expression ('string' (aka 'struct string') and 'string')
return  a / b ;

I think it's fine to just check for every type during compilation without complicating the language with concepts for now.

tobyjwebb commented 5 years ago

Today this compiles and crashes at run time yet:

I would treat it as a bug, and try to apply compile time checking on the generic function, and not modify the syntax.

tobyjwebb commented 5 years ago

PS: Regarding what I mentioned earlier, about the <> being required or not:

I would make them not required if you can infer the type from the arguments to the function, otherwise they would need to be explicitly defined.

fn foo<T>(f T) { ... }
fn bar<T>(x int) { /* do something with T. NOTE: T not used in parameters */ }
// <> not required to call foo:
foo('hello')
// <> required to call bar:
bar<App>(80)
avitkauskas commented 5 years ago

Oh yes, it really gives C error on compilation, not on run time. My bad. It's OK if it would be checked for every type during the compilation. Though it would restrict us to write generic functions that ONLY work for all types, we could not write then generic functions working only on numbers, or only on signed numbers etc.

gslicer commented 5 years ago

I'm not a big proponent of generics until it can be prevented that incompatible types (for the function operations) can be clearly sorted out (I think to ensure this function overloading would be necessary) e.g. adding two integers would call another function then adding (concatenating) two strings or adding two arrays of integers or adding two arrays of strings or... you get my point.

For example what happens if I provide a struct as T arg here:

fn devide<T>(a T, b T) T {
    return a/b
}

General question: are generics so beneficial at all that the clearity of syntax can be sacrificed for this matter? Probably V doesn't need its own version of any syntax construct of all other languages out there.

However this could be helpful: Generic Programming

Bunus reading: Advantage of Java Generics it seems like all 3 points mentioned there solve Java problems V does not have, or am I wrong?

medvednikov commented 5 years ago

For example what happens if I provide a struct as T arg here:

Compilation error. / cannot be used with this type.

I'm not a fan of generics either, but sometimes they are necessary.

The goal is to keep them small and simple.

gslicer commented 5 years ago

As long V detects such conflicts at compile-time it's fine for me, I would prefer the C++ like syntax having those where T could be any single upper-case letter and in case of multiple disjunct types <T,U> the compiler would actually also check: assert T.type() != U.type()

medvednikov commented 5 years ago

That's a good idea.

Not sure about allowing all letters yet. I like a limited set.

People can get really create with generics, I'd like to limit their usage a bit to achieve simpler code.

tobyjwebb commented 5 years ago

Why have assert T != U?

What if the user sometimes wants to pass the same type?

El mié., 23 oct. 2019 11:11, Alexander Medvednikov notifications@github.com escribió:

That's a good idea.

Not sure about allowing all letters yet. I like a limited set.

People can get really create with generics, I'd like to limit their usage a bit to achieve simpler code.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/vlang/v/issues/2469?email_source=notifications&email_token=AAE4HSNF6BJ6T336JY77SUDQQAIMDA5CNFSM4JC4T3T2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOECAVVRQ#issuecomment-545348294, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAE4HSIGDBGUMAACM5A24MTQQAIMDANCNFSM4JC4T3TQ .

gslicer commented 5 years ago

@tobyjwebb I think this is to improve clarity and readability - when the template creator designed the template to work with same types he should use the same letter for both arguments, don't you think?

tobyjwebb commented 5 years ago

What I'm saying is that it's not safe to assume that the T != U:

Say we have a generic function to create a map of type map[T]U

fn create_map<T, U>(k T, v U) {
   /// snip
}

What the assert T != U would imply is that the user couldn't create a map[string]string, for example (at least not with this function).

TW

On Wed, 23 Oct 2019 at 11:21, gslicer notifications@github.com wrote:

@tobyjwebb https://github.com/tobyjwebb I think this is to improve clarity and readability - when the template creator designed the template to work with same types he should use the same letter for both arguments, don't you think?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/vlang/v/issues/2469?email_source=notifications&email_token=AAE4HSM3P7CPFEYQWGGJ53DQQAJRRA5CNFSM4JC4T3T2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOECAWXSI#issuecomment-545352649, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAE4HSLVMN2SRTXDOVUIRZLQQAJRRANCNFSM4JC4T3TQ .

hamad-almamari commented 5 years ago

Why not just use like jai.

func foo(a $T, b T, c int) { °°°}

medvednikov commented 5 years ago

Why not just use like jai.

func foo(a $T, b T, c int) { °°°}

Interesting, we can simplify it even further, since V types can't be named T, U etc.

fn simple<T>(p T) T {
    return p
}

becomes

fn simple(p T) T {
    return p
}

@shsr04 what do you think?

This way there are no ugly <> at all.

medvednikov commented 5 years ago

@tobyjwebb you are right

What I'm saying is that it's not safe to assume that the T != U:

hamad-almamari commented 5 years ago

@medvednikov No what if we have custom type name T We put $T SO the type is exported.

Fun foo(a $T, b $A, c C) A {°°°}

$T and $A not defind types.

C is defind type just like int or whatever.

With $ we can easily handle the erros and tell the compiler and humens for what going stretfoard. Whithout any confusions.

shsr04 commented 5 years ago
fn simple(p T) T {
    return p
}

@medvednikov I prefer more detailed function declarations. I like to see with one look that a function is generic and which type params it uses. Being short is nice, but I don't see any meaningful advantage in removing the type parameter lists. And someone coming from Java or C++ can easily understand the good old <T, U> in a function declaration.

avitkauskas commented 5 years ago

What if all one capital letter names [A..Z] in V would be reserved for generic types only and this would be cleary stated in documentation? If you see one letter type, you know its generics. Then you can be short and clear (if you read the docs...) Just thinking...

medvednikov commented 5 years ago

Yes, that's the plan. One letter types only for generics.

Not sure if we should allow all 26 letters, or a limited set as it's implemented now.

medvednikov commented 5 years ago

@hamad-almamari

C is defind type just like int or whatever.

In V types can't be one letter long.

medvednikov commented 5 years ago

@shsr04 I see your point and I agree.

avitkauskas commented 5 years ago

@medvednikov Today this runs, so types can be single letter...

struct C {
    a int
}

fn main() {
    c := C{10}
    println(c)
}
tobyjwebb commented 5 years ago

Also, even if structs weren't allowed to be 1 letter long, what about if they were 2 letters? Yesterday I was looking at a bug report where the struct name was 2 letters long: struct St { ... }

fn generic(s St) { } // Easily confused with generics.

For something as "strange" as this, I would push for a more non-confusable syntax (like what is implemented at the moment).

fn do_something<A, B>(...) { } // I like this.

Also, it's the same syntax as in other languages, so it will be an easier transition, and another plus point is that it's already implemented, so it means less work.

On Wed, 23 Oct 2019 at 20:42, Alvydas Vitkauskas notifications@github.com wrote:

@medvednikov https://github.com/medvednikov Today this runs, so types can be single letter...

struct C { a int } fn main() { c := C{10} println(c) }

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/vlang/v/issues/2469?email_source=notifications&email_token=AAE4HSJJNFLN5H7LOBSWZLLQQCLITA5CNFSM4JC4T3T2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOECCOTDA#issuecomment-545581452, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAE4HSJ7KPEDPAL6JC6MTOTQQCLITANCNFSM4JC4T3TQ .

lcddh commented 5 years ago

I agree with medvednikov, can write one less word is one

medvednikov commented 5 years ago

@avitkauskas strange, I remember adding this check. I've just pushed a fix.

medvednikov commented 5 years ago

@tobyjwebb generics are one letter long. Anyway, I decided that current syntax is ok.

tobyjwebb commented 5 years ago

Anyway, I decided that current syntax is ok.

Excellent ^_^

On Wed, 23 Oct 2019 at 22:52, Alexander Medvednikov < notifications@github.com> wrote:

@tobyjwebb https://github.com/tobyjwebb generics are one letter long. Anyway, I decided that current syntax is ok.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/vlang/v/issues/2469?email_source=notifications&email_token=AAE4HSN6UHRJGSQIRL7SQYLQQC2Q5A5CNFSM4JC4T3T2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOECC2UXA#issuecomment-545630812, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAE4HSMVBCMOXG32Y3TYC6DQQC2Q5ANCNFSM4JC4T3TQ .

dhonx commented 5 years ago

I prefer the second syntax

gslicer commented 5 years ago

What's about assert T.type() != U.type() issue, if this is not true, why we need more than one single letter? We always could just use T or not?

teggotic commented 5 years ago

@medvednikov Why cant we do something like this: If we want to have parameter with generic type we can do:

//declaration => call
fn foo(bar T) => foo(8000)

But you can also have additional generic types:

fn foo<T,U>() => foo<FirstStruct, SecondStuct>()

And their combination:

fn foo<T, U>(bar V) => foo<FirstStruct, SecondStuct>(8000)

In this case we won't have to create any useless arguments like we do in our wveb:

//now
mut app := App{}
vweb.run(app, port)

//will be
vweb.run<App>(port)

And this pretty syntax will take it's place

foo(1)
foo('str')
foo(user)

if you declare foo like

fn foo(arg1 T)

It can be possible because we can't have type names in 1 character length

medvednikov commented 5 years ago

@Danil-Lapirow yes, I agree.

There are situations where we need to pass only types, like in the vweb example.

teggotic commented 5 years ago

We can still use <> in declarations to implicitly make generic functions

fn foo<>(bar T)
// it can be still called like
foo(1) 
// or
foo('baz')
danieldaeschle commented 4 years ago

why don't design the api of vweb like this?

fn main() {
    mut app = App{}
    app.run()
}
ghost commented 4 years ago

Hello! I just came up to this language a couple hours ago, and was easily hooked by its simplicity!

I really like the idea of delaying to check whether the applied types are valid or not to the use‐site (i.e. when the function is called).

I also really like the idea of dedicating specific names to be type parameters and not declaring them. It’s the same approach as Haskell takes, and it makes the declarations easier to read, I feel like.

I think it’d be neat if it’d be possible to infer types when a function is applied, and require explicit types if that is not the case.

The problem is that if the type parameters are not declared, it’s difficult to know which order to pass the type arguments in when calling the function.

dumblob commented 4 years ago

I recently noticed that Go will implement generics - feel free to take a look (it's a complete overhaul of the older official proposal by same authors - I heard this new proposal was made in collaboration with a Haskell developer/architect, but didn't take look who that was).

Maybe V can learn from some of the ideas and mistakes they did (will do).

@medvednikov have you seen this new draft already?