Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lambda Support #1066

Closed
N8python opened this issue Jul 9, 2019 · 43 comments
Closed

Lambda Support #1066

N8python opened this issue Jul 9, 2019 · 43 comments
Labels
Feature/Enhancement Request This issue is made to request a feature or an enhancement to an existing one.

Comments

@N8python
Copy link

N8python commented Jul 9, 2019

V is a concise and elegant language, and I think a syntax for lambda expressions could go along with the spirit of V. A lambda could be declared just like a function but without a name:

ints := [1, 2, 3]
doubled_ints := ints.map(fn (x int) {
  return x * 2
})

However, lambdas would have some additional convinces compared to regular functions. Target Typing would allow the lambda to drop the types on it's parameters.

doubled_ints := ints.map(fn (x) { return x * 2 })

Furthermore, if the lambda consists of a single return statement, the braces may be emitted and the return will be implicit:

doubled_ints := ints.map(fn (x) x * 2)

If the lambda doesn't take any parameters, the parens after fn may be omitted:

repeat(3, fn {
  println("Lambdas are awesome!")
})

The implicit return and parameterless fn could be combined:

repeat(3, fn println("Lambdas are awesome!"))

Closure could be implemented once V is more mature. For now, the ability to create anonymous functions would be a huge help to V programmers seeking a more functional style.

@N8python N8python added the Feature/Enhancement Request This issue is made to request a feature or an enhancement to an existing one. label Jul 9, 2019
@medvednikov
Copy link
Member

Hi,

Thanks. This was briefly discussed here:

#283

The syntax I'm considering is

doubled_ints := ints.map(fn (x) { return x * 2 })
==>
doubled_ints := ints.map(_ * 2)

One thing I'm worried about is that it's very easy easy to write unreadable code with complex nested maps.

I'm not sure what is the best way to prevent this.

@ntrel
Copy link
Contributor

ntrel commented Jul 10, 2019

_ * 2

Summarizing my comment from that issue:

That lambda syntax is not very general, it doesn't allow multiple lambda arguments. _ is the only clue in an expression that it's a lambda, it's quite cryptic - the argument doesn't always come first: 1 / _.
I suggest fn x! + y! if we want very short syntax. Other sigils can work instead of x!, e.g. \x, which Haskell uses for lambda parameters.

@N8python
Copy link
Author

I believe that a explicit declaration of the lambda but implicit parameter inference could work together:
The compact style of _ parameters can be confusing:

doubled_ints := ints.map(_ * 2) // What does _ mean? Is it a number? A variable?

However, a fn keyword could make this more clear:

doubled_ints := ints.map(fn _ * 2) //Oh, _ is part of a function

Parameters could be added at will:

doubled_ints := ints.map(fn (x) x * 2)

However, implicit return would aid in increasing readability

@ntrel
Copy link
Contributor

ntrel commented Jul 10, 2019

It's often good to have meaningful names for parameters to help the reader, even for lambdas. What if any single underscore prefixed identifiers _ident after fn are inferred parameters:

odd := container.filter(
    fn _elem & 1)
sum := container.reduce(
    fn _total + _elem)
container.sort(
    fn _a < _b)

@threeid
Copy link

threeid commented Jul 10, 2019

doubled_ints := ints.map({ return it * 2 }) like kotlin
or
doubled_ints := ints.map({ return $0 * 2 }) like swift

@N8python
Copy link
Author

N8python commented Jul 10, 2019

So, considering all your opinions, I think the syntax for lambda expressions should follow the following rules:

  1. When a fn keyword is found in a value position (ie. right-hand side of assignment, inside a parameter list, a const declaration) it is inferred to be a lambda. Lambdas are compile-time anonymous functions, which are given names for use at runtime. Lambdas create their own independent scope, and do not have closure. This means that lambdas do not capture their parent function's scopes. However, they can access consts , imported modules, and other top-level constructs (ie. functions, structs). The syntax of a lambda follows the following rules:

Lambdas in V would support Target Typing.
Ex:

fn map(arr int[], func fn (int) int) int[] {
  // Do the mapping
}

And later:

map([1, 2, 3], fn (x /* int type inferred */) /* return type int inferred */ { return  x * 2 })

Rule 1. All lambdas are prefixed with fn. Without the fn keyword, no lambda is inferred.

Rule 2. Lambdas follow the syntax regulations for normal functions, with the exception that the alphanumeric identifier after fn is absent. However, lambdas allow certain syntactic shorthands to increase readability.

Rule 3. Lambdas that take no parameters may omit the () after fn.
Ex:

repeat(3, fn {
  println("Hello World!")
})

Rule 4. Lambdas that consist of only one return statement may omit the return and the curly braces after the parameter list:

doubled_ints := ints.map(fn (x) x * 2)

Rule 5: Rule 3 and Rule 4 may be used together:

list_of_threes := ints.map(fn 3)

Rule 6: Marking a valid name with a ! at its end inside a lambda creates an inferred parameter. This inferred parameter is then (at compile-time) extracted into the parameter list. Two identical names marked with ! at their end are considered to refer to the same parameter. Since inferred parameters don't appear in the function's parameter list, they can be used in synergy with Rule 5:

doubled_ints := ints.map(fn x! * 2)
total := ints.reduce(fn total! + value!)

Translation to V (as of now):
Currently V offers no native support for lambdas. Lambdas without closure, however, would not require any modifications to the V Runtime (assuming V supports passing functions as parameters). At compile-time, V's compiler could extract lambdas into their own functions:
If the source code was:

fn main(){
  products := zipWith(fn x! * y!, [1, 2, 3], [4, 5, 6]) // ==> [4, 10, 18]
}

At compile time it would become:

fn lambda_1 /*unique name generated by compiler*/ (x int, y int) int {
  return x * y;
}
fn main(){
  products := zipWith(lambda_1, [1, 2, 3], [4, 5, 6]) // ==> [4, 10, 18]
}

Tell me what you think of this "proposal" for V lambda expressions.

@medvednikov
Copy link
Member

medvednikov commented Jul 10, 2019

V is a simple language. You just introduced 6 extra rules that have to be learned.

Rule 3 means that there are now 2 different ways to write lambdas without parameters. One of V's main philosophies is to have only one way of doing things.

So ideally, no new syntax would have to be introduced at all, just like in Go:

repeat(3, fn() {
  println('Lambdas are awesome!')
})
doubled_ints := ints.map(fn(n int) int { return n * 2 })

map, filter, etc often have one liners, so I propose the _ syntax similar to Scala's, that only works with one liners:

doubled_ints := ints.map(_ * 2)

@ntrel It won't be possible to declare a variable named _, so it won't be confusing.

For multiple arguments the traditional syntax will be used with full types specified. I think omitting the type is only readable when there's only one argument, the function is simple, and it's clear what the type is, like in the map example above.

@ntrel
Copy link
Contributor

ntrel commented Jul 11, 2019

I agree syntax rules should be simple. _ is a special syntax for lambdas that only take one unnamed argument. Instead of this special case, let's have a more useful general one:
fn (parameters) expression

This cuts out the return keyword and the noise of the braces, but still supports naming parameters and more than one parameter. It is also much closer to anonymous function syntax than just _ * 2, so it will be intuitive to anyone reading the code who is familiar with lambdas from many other languages. _ would not be familiar, they'd have to Google vlang underscore.
Multiple parameter lambdas are very common for any function that provides custom ordering, sort, merge etc.

For multiple arguments the traditional syntax will be used with full types specified

  1. If parameters are named, types are less needed.
  2. I'm not aware of any languages with generics that require types for lambda parameters. It's fine to require types for now, but later we should have inference for ease of use and easier reading of code.

@ntrel
Copy link
Contributor

ntrel commented Jul 11, 2019

it's very easy easy to write unreadable code with complex nested maps.

_ lambda syntax can't support nesting, my proposed general lambda syntax does:

// get strings that have whitespace
r := my_strings.filter(fn (s) s.can_find(
    fn (c) c.is_white()))

@N8python
Copy link
Author

@medvednikov makes a convincing argument. There should only be one way of doing things. V should support support lambdas that look just like functions without names. However, I still feel like the _ character is a little vague. An earlier proposal suggested something likes Swift's $0 inferred parameters. Perhaps the $num pattern could work? It allows flexible one liners, but also is more explicit than _. So we would have normal fns without names for lambdas, and then $nums for short one-liners. What do you think (I feel it is a balance between implicit and explicit)?

@watzon
Copy link
Contributor

watzon commented Jul 12, 2019

@medvednikov how would you feel about something like & instead of _? The main problem I see with the underscore is that it's easy to miss, whereas an ampersand is a little easier to spot. Your example would then become this:

doubled_ints := ints.map(& * 2)

It also has a familiarity to it for anyone that's used Ruby or SASS (maybe others too, idk).

@aguspiza
Copy link
Contributor

Why do not just use the same syntax that it is used in named fn?. Using any hidden trick like _, & or 'it' adds confusion to the reader, limits parameters to 1 and does not support nested lambdas.

You could try to remove {} as @N8python suggested to make it "cleaner" when it is a one liner:

doubled_ints := ints.map(fn (x) x * 2)

One thing to discuss should be if the return type is mandatory or always infered.

@N8python
Copy link
Author

N8python commented Jul 12, 2019

I agree with @aguspiza that the best syntax for one liners would be fn ([params]) [return value]. It's clear, explicit, and easier to understand than _ and &. The pattern fn ([params]) [return value] is actually implemented in Douglas Crockford's transpiled language Neo. As for return value inference, for a one liner it shouldn't be any harder than inferring a type for a variable.

@watzon
Copy link
Contributor

watzon commented Jul 12, 2019

@aguspiza the point behind the shorter syntax is just that, it's shorter and more concise for simple operations. I know in Ruby I use it all the time for Array#map. Instead of doing this arr.map { |i| i.inc } you can do arr.map(&.inc).

I know some people aren't fans of syntactic sugar like that, but it does make things a bit more concise. Of course we should still have a full lambda syntax, and for that I agree that the ints.map(fn (x) x * 2) syntax makes the most sense.

@medvednikov
Copy link
Member

Using any hidden trick like _, & or 'it' adds confusion to the reader

It's used in many popular languages with functional features like Scala, Swift, Ruby, Kotlin. I wouldn't call it confusing.

Of course we should still have a full lambda syntax, and for that I agree that the ints.map(fn (x) x * 2) syntax makes the most sense.

So now we have 3 lambda syntaxes :)

@watzon
Copy link
Contributor

watzon commented Jul 12, 2019

@medvednikov 3? What's the third? I only see the short syntax and the long syntax

@medvednikov
Copy link
Member

medvednikov commented Jul 12, 2019

@watzon

  1. ints.map(fn (x int) { return x * 2 })
  2. ints.map(fn (x) x * 2)
  3. ints.map(& * 2)

@N8python
Copy link
Author

N8python commented Jul 12, 2019

I feel like lambda syntax is very complicated as it is implemented in so many different ways. Ie. Javascript has 6 different ways of writing the same line of code for doubling all ints in an array:

ints.map(x => x * 2);
ints.map((x) => x * 2);
ints.map(x => { return x * 2 });
ints.map((x) => { return x * 2 });
ints.map(function(x){return x * 2 });
ints.map(function doubler(x){ return x * 2 });

Don't even get me started on Scala.

V, being revolutionary in its one-way-only idea (lots of languages CLAIM to have "one way" to do something, but they have 3 or 4), should perhaps only have ONE lambda syntax: the original fn syntax without a name. No implicit returns, no syntactic shorthands. This would be different, but WAY more explicit. What do you think?

@medvednikov
Copy link
Member

@N8python

I think it's a very good option, if you mean only having ints.map(fn (x int) { return x * 2 })

More typing, but having only one way fits really well with V's philosophy.

@alfclement
Copy link

I like : ints.map(fn (x int) { return x * 2 })
This is clear and clean...

Can you declare "local" functions like:

fn main() {
     a:=3
     c:= 4
     fn check(x int) bool {
        // some checks here
        return true
     }
     check(a)
     check(b)

Of course inline code ;-)

@N8python
Copy link
Author

I think you would declare inline functions as:

check := fn (x int) bool {
   return true
}

@N8python
Copy link
Author

I think we are close to reaching a sort-of consensus. Right now, I agree with @medvednikov that V should only have one way to do lambdas, even if it means extra typing. Explicit typing in lambdas may be cumbersome, but, as @medvednikov said, lambdas should be close to regular functions.
Tell me if you think this would be a good resolution to lambda expressions.

@watzon
Copy link
Contributor

watzon commented Jul 12, 2019

If we're going to go that route, why not just make functions first class citizens? Then not only could you create a lambda by assigning a function to a variable, but you could also pass other non-lambda functions around. Thoughts?

@N8python
Copy link
Author

I agree. I think V would benefit greatly from first-class functions. The only thing I'm not sure about is closure. V needs to be fast, and closure could slow it down. @medvednikov, what would you think about making functions first-class citizens but not implementing closure?

@ntrel
Copy link
Contributor

ntrel commented Jul 13, 2019

fn (x int) { return x * 2 }

I don't think that is a lambda, just an anonymous function. A lambda does not have statements.

@ntrel
Copy link
Contributor

ntrel commented Jul 13, 2019

Closures are fast if they don't escape the scope they are declared in, then they don't need to be heap allocated. GNU C supports these closures.
(There is a minor cost that closures have a function pointer and a context pointer, but this is fairly trivial).

@N8python
Copy link
Author

So... you think that V should allow first-class functions and closure, but not allow "heap closure", where the variables escape the function? That sounds reasonable.

@watzon
Copy link
Contributor

watzon commented Jul 13, 2019

I agree. First class functions would be an amazing addition, but closures should definitely be a thing. Not allowing "heap closures" would be a good compromise to keep performance up.

@N8python
Copy link
Author

So, our consensus is to allow anonymous functions, first-class functions, and closure, but not "heap closure". Please point out if this is not a satisfactory resolution.

@watzon
Copy link
Contributor

watzon commented Jul 16, 2019

Shouldn't this wait to be closed until the feature is actually added?

@N8python
Copy link
Author

N8python commented Jul 16, 2019 via email

@avitkauskas
Copy link
Contributor

avitkauskas commented Aug 27, 2019

Having closure syntax as anonymous functions is fine and is very inline with the "one-way" philosophy. But there are two different places where they are used, and I would like to have different approach towards argument types declaration: one thing when you declare the function taking function or closure as argument, another one is when you use it:

fn (a mut []int) map(func fn (x int) int) { ... }

but

[1, 2, 3].map(fn (x) x * 2)

I would like to have it this way.

@siloam
Copy link

siloam commented Sep 1, 2019

Actually ints.map((x) => { return x * 2 } syntax let you write any lambda in JS. Implementing was NOT very hard so they create additional options to do so (some archaic ones are with 'function' keyword). In my opinion lambda syntax SHOULD be different because it lets you find where lambda function is very quickly. If you will have just fn keyword and nest several functions in one another it will be hard to check what's going on. Here you have two factors: no name and arrow syntax. Look at Pony, Haskell, Coconut etc.

@N8python
Copy link
Author

N8python commented Sep 1, 2019

I understand. However, I dislike the arrow syntax if you already have the fn keyword. I use arrow function syntax in JavaScript, and I use function for my top-level functions. This is unfortunate, I would have liked a more concise syntax based of function instead of arrows. Since V wants only one way to do things, I think

ints.map(fn (x) x * 2)

would be a good compromise.

@gslicer
Copy link

gslicer commented Sep 2, 2019

I think it's a very good option, if you mean only having ints.map(fn (x int) { return x * 2 })

@medvednikov I think this is the best way to go, anonymous (lambda) function definition shall be exactly like a standard definition, but without the need of a name

I also encourage that functions in general require at least one "return" statement, there should be no implicit return values allowed, which makes it more readable especially when there is nothing to return like "none".

@ntrel
Copy link
Contributor

ntrel commented Sep 3, 2019

@siloam

lambda syntax SHOULD be different because it lets you find where lambda function is very quickly

You could find them all with a regexp, although probably you'd be looking for a specific usage instead and search for e.g. find(fn.

If you will have just fn keyword and nest several functions in one another it will be hard to check what's going on.

This seems fine:

strings.filter(
  fn (s) s.find(
    fn (c) c.is_alpha()) != none)

Given that if and match expressions implicitly yield a result, it seems a bit odd not to have that for lambdas, which are meant to be short (otherwise they're just function literals). Even requiring braces without return would be better than requiring return.

@hazae41
Copy link

hazae41 commented Oct 3, 2019

How about the "it" keyword? Like in Kotlin lambdas:

Short lambda

ints.map(fn it * 2);

Long lambda

ints.map(fn {
  return it * 2;
});

Typed short lambda

ints.map(fn (x int) x * 2);

Typed long lambda

ints.map(fn (x int) int {
  return x * 2;
});

"Brackets" short lambda

ints.map{ it * 2 };

"Brackets" long lambda

ints.map{
  return it * 2;
};

Lambda with receiver

show := fn (this string) () {
  println(this);
};

"Hello World".show();

@medvednikov
Copy link
Member

Short optimized filter/map have been implemented:

nums := [1, 2, 3, 4, 5, 6]
even := nums.filter(it % 2 == 0)
println(even) // [2, 4, 6]

words := ['hello', 'world']
upper := words.map(it.to_upper())
println(upper) // ['HELLO', 'WORLD']

For everything else the same syntax (fn(x int) int {) will be used.

@N8python
Copy link
Author

N8python commented Nov 1, 2019

That's awesome! I like the balance between implicit and explicit - shorthand syntax for common cases, and more verbose syntax for the uncommon ones. This is a great resolution.

@medvednikov
Copy link
Member

I like the balance between implicit and explicit - shorthand syntax for common cases, and more verbose syntax for the uncommon ones.

That was exactly the reasoning :) Glad you like it.

@wdonne
Copy link

wdonne commented Dec 21, 2020

I think the real issue is type inference, which is very powerful, but not easy to implement. The syntax fn (x) x * 2 for lambdas would not have to be new special syntax if regular functions could also be declared like this, but with a name then. The type of x would be inferred everywhere the function is called.
Making the curly braces and the return keyword optional is also not a new way to do something that can already be done. It is only omitting redundant information that can be inferred.
Deep type inference will slow down the compiler, but also increase correctness.

@ntrel
Copy link
Contributor

ntrel commented Dec 21, 2020

Return type inference is very easy to implement and would not slow down the compiler:

fn (x int) {x * 2}

I would limit it to a single expression.

shorthand syntax for common cases

map, filter and sort (each only on arrays) does not cover all the common cases. find(fn(E)bool) R is a very common functional method, as is reduce. Probably quite a few others too. Built-in methods is not a good way to implement each common case.

@IngwiePhoenix
Copy link

Still hoping for a syntax where the callback's typing is infered from the initial definition. Having only a few methods sprinkled throughout with the it compiler-magic syntax is a bit... odd.

Hoping this will bepicked back up eventually and improved on :)

@vlang vlang locked and limited conversation to collaborators Sep 22, 2021

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
Feature/Enhancement Request This issue is made to request a feature or an enhancement to an existing one.
Projects
None yet
Development

No branches or pull requests