Draco-lang / Language-suggestions

Collecting ideas for a new .NET language that could replace C#
75 stars 5 forks source link

[WIP] Lambdas #49

Open WhiteBlackGoose opened 2 years ago

WhiteBlackGoose commented 2 years ago

Jump to

Overview

Lambda functions (or anonymous functions) are functions or methods that have no statically resolved name, they are usually bound to a local variable or passed as an argument straight away. Their primary use in the .NET ecosystem is passing around single-use methods, like for configuration, factories or inside LINQ methods. They are also usually the primary way to write closures.

Requirements towards the feature

In addition to being very concise and readable, I believe it should not diverge from regular functions too much. C# started out with a very different mechanism, but lambdas are slowly being merged together with regular methods in terms of functionality. As we have been suggested, we might be best off by shaving our original functions to get something more compact.

Syntax

Requirements

It should be very concise and readable.

C# way

Definition:

() => 5
a => 5
(int a) => 5
(int a, string b) => 5
... => { return 5; }

Type declaration:

Func<T1, T2, ..., TOut>

Kotlin way

Definition:

{ 5 }
{ it + 5 } // "it" is the default parameter name for single-parameter lambdas
{ a: int -> 5 }
{ a: int, b: string -> 5 }
{ ... -> return 5; }

Type declaration:

(T1) -> T2
(T1, T2) -> T3
(T1) -> (T1, T2)

Implicit lambdas

As I've mentioned before, a large percentage of lambdas (mostly in LINQ) create a lot more noise around them than what they should be. Examples:

var ys = xy.Select(x => x.ToFoo()); // Calling a member function on each requires a full lambda
var zs = ns.Select(x => x + 1); // Incrementing each number requires a lambda
var ws = ns.Select(x => Compute(x, 1)); // Just because I have another constant parameter, I need a lambda

You might say that currying or partial application can solve this, and yes, they do somewhat. But a more general feature could be to promote expressions with implicitly defined lambda parameters to be lambda expressions themselves. The previous examples with a made-up syntax, where $n means the nth lambda parameter:

var ys = xy.Select($0.ToFoo());
var zs = ns.Select($0 + 1);
var ws = ns.Select(Compute($0, 1));

This is more general in the sense, that it even works with multiple arguments and they can be reordered in any way.

Note that there is still a big, unresolved problem with this idea! Namely, it is unclear what subexpressions to promote to lambdas, where the "boundaries" are. This will likely require some separator characters, like in Swift or Kotlin.

Wip...

Internals

There are three ways

  1. Using .NET's Func and Action
  2. Creating our own type similar to how it does F#
  3. Creating our own interface to allow the user to manually implement it

Using .NET's Func and Action

Pros:

  1. 100% compatibility with C#
  2. No overhead converting/invoking Fresh's delegates into C#'s (because the same type)

Cons:

  1. C# delegates so far always add overhead.
  2. Two different things: Func and Action, whereas it'd be nice to have one

Creating our own type similar to how it does F

Pros:

  1. We can inline them ourselves somehow (?)
  2. We have more control over them (e. g. we can add source code for string representation of a function in debug mode 🤔 )
  3. Can be implicitly converted to C#'s delegates and back

Cons:

  1. Incompatible with C# directly, so conversion (and potentially every invokation) will have an overhead

Creating our own interface to allow the user to manually implement it

Pros:

  1. Performance without hacks. All we need is to auto-generate readonly lambdas in structs and constrain callees with expected function to the given interface
  2. Flexibility - the user may want to implement the method themselves for some particular reason (eliminating any overhead at all, especially if we get extensions)

Cons:

  1. Not compatible with C#
  2. No implicit operators definable either - we will to either detect this case ourselves or write extension methods (or both) to convert forth and back
LPeter1997 commented 2 years ago

Some additions. If you want to, you can merge in some of them to the op., I don't want to edit your issue without you knowing it.

Overview

Lambda functions (or anonymous functions) are functions or methods that have no statically resolved name, they are usually bound to a local variable or passed as an argument straight away. Their primary use in the .NET ecosystem is passing around single-use methods, like for configuration, factories or inside LINQ methods. They are also usually the primary way to write closures.

Requirements towards the feature

In addition to being very concise and readable, I believe it should not diverge from regular functions too much. C# started out with a very different mechanism, but lambdas are slowly being merged together with regular methods in terms of functionality. As we have been suggested, we might be best off by shaving our original functions to get something more compact.

Implicit lambdas

As I've mentioned before, a large percentage of lambdas (mostly in LINQ) create a lot more noise around them than what they should be. Examples:

var ys = xy.Select(x => x.ToFoo()); // Calling a member function on each requires a full lambda
var zs = ns.Select(x => x + 1); // Incrementing each number requires a lambda
var ws = ns.Select(x => Compute(x, 1)); // Just because I have another constant parameter, I need a lambda

You might say that currying or partial application can solve this, and yes, they do somewhat. But a more general feature could be to promote expressions with implicitly defined lambda parameters to be lambda expressions themselves. The previous examples with a made-up syntax, where $n means the nth lambda parameter:

var ys = xy.Select($0.ToFoo());
var zs = ns.Select($0 + 1);
var ws = ns.Select(Compute($0, 1));

This is more general in the sense, that it even works with multiple arguments and they can be reordered in any way.

Note that there is still a big, unresolved problem with this idea! Namely, it is unclear what subexpressions to promote to lambdas, where the "boundaries" are. This will likely require some separator characters, like in Swift or Kotlin.

eatdrinksleepcode commented 2 years ago

C# started out with a very different mechanism, but lambdas are slowly being merged together with regular methods in terms of functionality.

I believe that a major reason that they were treated so differently is that the main driver of getting them added to the language was not just LINQ but LINQ-to-SQL and Entity Framework; and L2S/EF required expression trees, which needed to be restricted to a subset of the full language to prevent L2S/EF from needing to create a full compiler to be able to convert the lambda into SQL.

Expression trees were (and are) a powerful addition to the language, but being restricted by their intended use case makes them much less useful than they could be. They also have not kept up well with the evolution of the language, likely to keep providers from needing to constantly play catch up.

where $n means the nth lambda parameter

it is so useful because it is unambiguous in single-parameter lambdas, which are extremely common. If I'm e.g. iterating a collection, it is obvious what it refers to. I worry that as soon as you have more than one parameter, referring to them anonymously will make the code much less readable.

This will likely require some separator characters, like in Swift or Kotlin.

Some examples would be useful here.

LPeter1997 commented 2 years ago

Some examples would be useful here.

So my fear is that simply something like ns.Foo($0 + 1) can be promoted to lambda in so many ways:

A separator character, like { } could somewhat disambiguate this: ns.Foo({ $0 + 1 }) but then we are not much better than just having the arrow syntax instead imo.

WhiteBlackGoose commented 2 years ago

(Inserted LPeter's comment)

LPeter1997 commented 2 years ago

I'd like to add an idea for syntax, which might not be popular, it is literally just getting rid of the name from the regular function syntax.

C#:

() => 5
a => 5
(int a) => 5
(int a, string b) => 5
... => { return 5; }

Fresh:

func() = 5
func(a) = 5
func(a: int32) = 5
func(a: int32, b: string) = 5
func(...) { return 5; }

I'm not sure what we should do with the return type here, optionally we could allow the user to specify it the usual way. Allowing inference here implicitly might be a good idea.

rikashore commented 2 years ago

I think keeping your own type would be convenient if you let unit or void be usable as a type parameter. This is pretty much the reason the distinction between Action and Func exists in my opinion.