dotnet / vblang

The home for design of the Visual Basic .NET programming language and runtime library.
290 stars 64 forks source link

`Def` - alternative lambda definition or maybe local functions #221

Open rskar-git opened 6 years ago

rskar-git commented 6 years ago

In a nutshell:

' Single line Function
Def Function <identifier> (<param-list>) [As <type>] <single-expression>

' Single line Sub
Def Sub <identifier> (<param-list>) <single-statement>

' Multi-line Function
Def Function <identifier> (<param-list>) [As <type>]
   <code-block>
End Function 

' Multi-line Sub
Def Sub <identifier> (<param-list>)
   <code-block>
End Sub 

The keyword Def is suggested on the basis of old BASIC (DEF FN) and Python. The Def has an identifier that is taken to be a Delegate that is ReadOnly. Perhaps some compiler optimizations can be made from that ReadOnly condition (e.g. https://docs.microsoft.com/en-us/dotnet/csharp/local-functions-vs-lambdas).

Example:

Imports System Imports System.IO;

Class Example

Shared Sub Main()
    Dim contents  As string = GetText("C:\temp", "example.txt")
    Console.WriteLine($"Contents of the file:{vbCrLf}" + contents)
End Sub

Private Shared Function GetText(string path, string filename) As String 

    Def Function AppendPathSeparator(filepath As String) As String
        If (Not filepath.EndsWith("\")) Then filepath += "\"
        Return filepath
    End Function

    Dim sr = File.OpenText(AppendPathSeparator(path) + filename)
    Dim text = sr.ReadToEnd()
    return text

End Function

End Class

AnthonyDGreen commented 6 years ago

Oooh. Love the callback to Def Fn!

That said, C# lambdas are waaaaay behind VB lambdas in terms of their capabilities (e.g. VB lambdas can be iterators). That's why local functions makes more sense in C#. But in VB you can literally write this today:

Private Shared Function GetText(path As String, filename As String) As String 

    Dim AppendPathSeparator = Function(filepath As String) As String
                                  If Not filepath.EndsWith("\") Then filepath &= "\"
                                  Return filepath
                              End Function

    Dim sr = File.OpenText(AppendPathSeparator(path) & filename)
    Dim text = sr.ReadToEnd()
    Return text

End Function

And the compiler already does some optimizations if the anonymous delegate is only ever converted to a nominal delegate (it elides it) so it just shorter to optimize the lambda case than to make a whole new feature. Of course, either way we'd need some compelling real-world performance data to tell us that's what we need to do. The actual way local functions in C# is implemented is... monstrous.

Of course, your proposal raises a good point about the order of things Name = Function(params) as opposed to Function Name(params). We could support it as a synonym syntax. I'm of mixed feelings. It's worth thinking about. Thanks!

reduckted commented 6 years ago

your proposal raises a good point about the order of things

Are you suggesting that perhaps we could declare the lambda like this:

Dim Function AppendPathSeparator(filepath As String) As String
    If Not filepath.EndsWith("\") Then filepath &= "\"
    Return filepath
End Function

The one thing that bugs me about writing lambdas as though they are inline functions today is the indentation. The "inline function" is waaayyy over to the right because of the local variable declarator.

Nukepayload2 commented 6 years ago

We can't write recursion in lambdas today, but recursion is allowed in C# local functions. So, this proposal makes sense.

' method-scoped code
Const MaxDepth = 32
Dim getFiles =
        Iterator Function(dir As String, iterCount As Integer) As IEnumerable(Of String)
                For Each f In Directory.GetFiles(dir)
                    Yield f
                Next
                If iterCount >= MaxDepth Then Return
                For Each innerDir In Directory.GetDirectories(dir)
                    For Each f In getFiles(innerDir, iterCount + 1)
                        Yield f
                    Next
                Next
        End Function
For Each system32Files In getFiles("C:\Windows\System32", 1)
    ' use
Next
rskar-git commented 6 years ago

@Nukepayload2 Turns out recursion in lambdas is possible, but the syntax is uglied:

    Const MaxDepth = 32
    Dim getFiles As Func(Of String, Integer, IEnumerable(Of String))
    getFiles = Iterator Function(dir As String, iterCount As Integer) As IEnumerable(Of String)
                   For Each f In IO.Directory.GetFiles(dir)
                       Yield f
                   Next
                   If iterCount >= MaxDepth Then Return
                   For Each innerDir In IO.Directory.GetDirectories(dir)
                       For Each f In getFiles(innerDir, iterCount + 1)
                           Yield f
                       Next
                   Next
               End Function
    For Each system32Files In getFiles("C:\Windows\System32", 1)
        ' use
        Console.WriteLine(system32Files)
    Next

@AnthonyDGreen Is the compiler still able to optimize this?

Nukepayload2 commented 6 years ago

@rskar-git That's recursion in Func(Of String, Integer, IEnumerable(Of String)) rather than lambdas. getFiles is not anonymous delegate any more, because you have specified its type via As. Besides, your code can be simplified.

Const MaxDepth = 32
Dim getFiles As Func(Of String, Integer, IEnumerable(Of String)) =
    Iterator Function(dir, iterCount)
        For Each f In IO.Directory.GetFiles(dir)
            Yield f
        Next
        If iterCount >= MaxDepth Then Return
        For Each innerDir In IO.Directory.GetDirectories(dir)
            For Each f In getFiles(innerDir, iterCount + 1)
                Yield f
            Next
        Next
    End Function
For Each system32Files In getFiles("C:\Windows\System32", 1)
    ' use
    Console.WriteLine(system32Files)
Next
rskar-git commented 6 years ago

@Nukepayload2 You point out a subtle difference - and I agree that this difference can be big performance-wise (not to mention what if getFiles got reassigned between calls?).

@AnthonyDGreen (and Nukepayload2) I learned from Jon Skeet that there's no such thing as "Anonymous delegate" (https://stackoverflow.com/questions/1748719/c-sharp-anonymous-delegate). Of course, that was back in 2009, things could be different now.

Anyway, would this Def concept be of any help to the recursion use-case?

pricerc commented 5 years ago

@rskar-git Thanks for highlighting this.

If I had to prioritise the usefulness of local functions, lambdas and anonymous methods/delegates, it would be in that order.

I've always been uneasy around lambdas. I suspect because of my early formal training in structured programming using Pascal, I find them "ill-defined".

I think this proposal offers a really good option for "well defined" lambdas and/or 'local functions'.

I also like the distinction introduced by a 'Def' instead of a 'Dim' - Dim implies a place to store data(which I find a weird place to put a function), whereas 'Def' would be implying a code block.

Actually, slightly off-topic; I think a possible reason I'm not a huge fan of 'first-class functions' is because of a sub-conscious bias - having code and data sharing memory is considered (by some/many?) a security risk, to the point where some computer architectures don't allow the same memory block to be used for both code and data. And 'Dim'ming a 'Function' looks like putting code where data should be. Now that I've recognised that, I should be able to work on my phobia :)

VBAndCs commented 5 years ago

I see no need for anything other than lambdas. But I like the Dim Function suggestion. We can write recursive lambdas today:

Dim Factorial As Func(Of UInteger, Double) = 
             Function(n) If(n = 0, 1, n * Factorial(n - 1))

We just can't use type inference with recursion. If you want to have it, I suggest this:

Dim Factorial = 
             Function(n) If(n = 0, 1, n * Function(n - 1))

But in fact, I don't like using the Function keyword with inline lambdas, because it leads to too long expressions. I heared many saying they love C# lambdas over VB's because of this. I like to use the Fn keyword instead.. Somthing like that: Dim x = Foo(Fn(m) => m + 1 )

pricerc commented 5 years ago

Ok, so you'll have to explain to me how:

Dim Factorial As Func(Of UInteger, Double) = Function(n) If(n = 0, 1, n * Factorial(n - 1))

or

Dim Factorial = Function(n) If(n = 0, 1, n * Function(n - 1))

would be easier to use than

Dim Function Factorial(n As UInteger) As Double = If(n = 0, 1, n * Factorial(n - 1))

(I've used Dim to illustrate the point, although I think Def would be a better keyword)

pricerc commented 5 years ago

Dim x = Foo(Fn(m) => m + 1 )

AARGH MY EYES!

I know that people don't like typing, and the word Function is a whole 8 characters, but really, how often do you type more than the first two before intellisense kicks in and offers you the rest? Every language has trade-offs, otherwise everyone would use the same language. VB requires what some see as 'verbosity', but what I see as 'clarity'. To me, this is a worthwhile trade-off.

Also, what would that code even do?

You'd really like good 'ol Sinclair BASIC:

DEF FN H$(n)=CHR$ (INT (n/4096)+7*(INT (n/4096)>9)+48)+CHR$ (INT (FN V(n,4096)/256)+7*(INT (FN V(n,4096)/256)>9)+48)+CHR$ (INT (FN V(FN V(n,4096),256)/16)+7*(INT (FN V(FN V(n,4096),256)/16)>9)+48)+CHR$ ((FN V(FN V(FN V(n,4096),256),16))+7*(INT FN V(FN V(FN V(n,4096),256),16)>9)+48)

they love C# lambdas over VB's

Then let them use C#!

VBAndCs commented 5 years ago

@pricerc You always remind me not to suggest another way to do the same thing :)

The issue with dim x = Foo(Function(x, y, z) x + y + z) is not about typing, but reading. I always take some time gazing at such lambda feeling confused. The expression is long and eyes distracting. There is no separation between method header and body. There is no indication of a return value. This is the worst VB syntax I ever dealt with. This is why I like this: dim x = Foo(Fn(x, y, z) => x + y + z) It overcomes the stated issues, and is more meaningful and more readable than C#'s var x = Foo((x, y, z) => x + y + z); So, it is a VB-stylish for me. In fact I will add it to ZML. I avoided the vb lambdas by adding a <lambda> tag, but the Fn( ) => is an easy solution, and all I need is to remove the Fn to have the C# lambda :). I'll try it.