C# like lambdas and closures

Monkey Forums/Monkey Code/C# like lambdas and closures

Erik(Posted 2014) [#1]
preprocessor.monkey
' ------------------------------------------------------
' -                                                    -
' -        START MACRO CODE                            -
' -                                                    -
' ------------------------------------------------------




Global lastStrFromTo
' Returns "" if not found, does not include fromstr and tostr in the result
Function StrFromTo:String(s:String, fromstr:String, tostr:String)
  If s="" Return ""
  Local f = s.Find(fromstr)
  lastStrFromTo = f
  If f = -1 Then Return ""

  f += fromstr.Length()
  Local t = s.Find(tostr,f)
  If t = -1 Then Return ""

  Return s[f..t]
End
' Returns "" if tostr is not found, does not include tostr in the result
Function StrTo:String(s:String, tostr:String)
  Local t = s.Find(tostr)
  If t = -1 Then Return ""
  Return s[..t]
End
' Returns "" if fromstr is not found, does not include fromstr in the result
Function StrFrom:String(s:String, fromstr:String)
  Local f = s.Find(fromstr)
  If f = -1 Then Return ""
  Return s[f+fromstr.Length..]
End
Function IsWhitespace?(c)
  Return c = $20  Or  (c >= $09  And  c <= $0d) Or  c = $85  Or  c = $A0
End

Function GetLongType:String(s:String)
  s = s.Trim()

  If s.Contains(":") Then
    Return StrFrom(s,":")
  Else
    If s="$" Then Return "String"
    If s="#" Then Return "Float"
    If s="?" Then Return "Bool"

    If s.EndsWith("$") Then Return "String"
    If s.EndsWith("#") Then Return "Float"
    If s.EndsWith("?") Then Return "Bool"
  End

  Return "Int"
End
Function RemoveType:String(s:String)
  s = s.Trim()
  If s.Contains(":") Then
    Return StrTo(s,":")
  Else
    If s.EndsWith("%") Then Return s[..-1]
    If s.EndsWith("$") Then Return s[..-1]
    If s.EndsWith("#") Then Return s[..-1]
    If s.EndsWith("?") Then Return s[..-1]
  End

  Return s
End

Global AnonFuncCount=0
Global AnonActionCount=0
Function ConvertMacros:String(tmp:String)
  Local code := StrFromTo(tmp,"{(","}")
  Local foundmacro = code.Find("=>")

  While foundmacro>-1
    Local codebefore := code
    Local paramstr:String
    Local params:String[]

    Local commented?
    For Local i := lastStrFromTo To 0 Step -1
      If tmp[i]=13 Or tmp[i]=10 Then
        Local codeline := tmp[i..i+100].Trim()
        commented = codeline[0] = "'"[0]
        Exit
      End
    End

    If codebefore.Find("~n")>-1 Then
      linesRemovedByMacros+=(codebefore.Split("~n").Length-1)
    End

    If commented Then
      tmp = tmp.Replace("{("+codebefore+"}", "")
      code = StrFromTo(tmp,"{(","}")
      foundmacro = code.Find("=>")
      Continue
    End

    For Local i := 0 Until code.Length
      If code[i] = "("[0] Then Exit
      If code[i] = ")"[0] Then
        paramstr = code[0..i]
        If paramstr.Trim()<>"" Then
          params = paramstr.Split(",")
        End
        Exit
      End
    End
    Local returnstr := StrFromTo(code,")","=>").Trim()
    Local returnType$ = GetLongType(returnstr)

    code = code[foundmacro+2..]

    ' If just a one line function, insert return
    If returnstr<>"" And code.Find("~n")=-1 Then
      code = "Return "+code.Trim()
    End

    Local isFunction?=False
    Local ret := code.ToUpper().Find("RETURN ")
    If ret>-1 Then
      For Local i := ret Until code.Length
        If code[i]=13 Or code[i]=10 Or code[i]="'"[0] Then
          Exit
        End

        If Not IsWhitespace(code[i]) Then
          isFunction = True
          Exit
        End
      End
    End

    Local paramtypes := ""

    For Local i := 0 Until params.Length
      paramtypes += GetLongType(params[i])+","
    End

    Local suffix := ""
    If params.Length>0 Then
      suffix = params.Length
    End

    Local AddCode := ""
    Local globalVarName := ""

    ' Check for variable captures (closure)
    code = code.Replace("||","___OR___")

    Local fieldstr := ""
    Local fields:String[]
    Local AddFields := ""
    Local Callfields := ""
    Local InitFields := ""

    Local capture := StrFromTo(code,"|","|")

    While capture<>""
      fieldstr += capture+","

      AddFields += "  Field "+capture+"~n"
      Local callfield := RemoveType(capture)
      InitFields += "    Self."+callfield+"="+callfield+"~n"
      Callfields += callfield+","

      code = code.Replace("|"+capture+"|",callfield)
      capture = StrFromTo(code,"|","|")
    End

    'Remove last commas
    If fieldstr<>"" fieldstr = fieldstr[..-1]
    If Callfields<>"" Callfields = Callfields[..-1]

    code = code.Replace("___OR___","|")

    If isFunction Then
      If paramtypes<>"" Then
        paramtypes = "<"+paramtypes+returnType+">"
      End

      If fieldstr="" Then
        AddCode = "Class AnonFunc__"+AnonFuncCount+" Extends Func"+suffix+paramtypes+"~n"+
                   "  Method Do:"+returnType+"("+paramstr+")~n    "+
                   code.Trim()+"~n"+
                   "  End~n"+
                   "End~n"+
                   "Global "+globalVarName+" := New AnonFunc__"+AnonFuncCount+"()~n~n"
        globalVarName = "AnonFuncCall__"+AnonFuncCount
      Else
        AddCode = "Class AnonFunc__"+AnonFuncCount+" Extends Func"+suffix+paramtypes+"~n"+
                   AddFields+
                   "  Method New("+fieldstr+")~n"+
                   InitFields+
                   "  End~n"+
                   "  Method Do:"+returnType+"("+paramstr+")~n    "+
                   code.Trim()+"~n"+
                   "  End~n"+
                   "End~n"
        globalVarName = "(New AnonFunc__"+AnonFuncCount+"("+Callfields+"))"
      End

      AnonFuncCount += 1
    Else
      If paramtypes<>"" Then
        paramtypes = "<"+paramtypes[..-1]+">" 'Remove last comma
      End

      If fieldstr="" Then
        AddCode = "Class AnonAction__"+AnonActionCount+" Extends Action"+suffix+paramtypes+"~n"+
                  "  Method Do:Void("+paramstr+")~n    "+
                  code.Trim()+"~n"+
                  "  End~n"+
                  "End~n"+
                  "Global "+globalVarName+" := New AnonAction__"+AnonActionCount+"()~n~n"
        globalVarName = "AnonActionCall__"+AnonActionCount
      Else
        AddCode = "Class AnonAction__"+AnonActionCount+" Extends Action"+suffix+paramtypes+"~n"+
                  AddFields+
                  "  Method New("+fieldstr+")~n"+
                    InitFields+
                  "  End~n"+
                  "  Method Do:Void("+paramstr+")~n    "+
                    code.Trim()+"~n"+
                  "  End~n"+
                  "End~n"
        globalVarName = "(New AnonFunc__"+AnonActionCount+"("+Callfields+"))"
      End

      AnonActionCount += 1
    End
    Print "Macro:"+globalVarName+"="
    Print AddCode

    tmp = tmp.Replace("{("+codebefore+"}", globalVarName)+"~n"+AddCode
    code = StrFromTo(tmp,"{(","}")
    foundmacro = code.Find("=>")

  End
  Return tmp
End

Function MacroExpand:String(path:String)
  Local tmp := LoadString( path )
  Return ConvertMacros(tmp)
End

Global linesRemovedByMacros
Function PreProcess$( path$,modpath$="" )
  linesRemovedByMacros = 0
	Local cnest,ifnest,line,source:=New StringStack
	
	Local toker:=New Toker( path, MacroExpand(path) )
	toker.NextToke

  If linesRemovedByMacros>0 Then
    Print "("+path+":~nlines removed:"+linesRemovedByMacros+")"
  End

' ------------------------------------------------------
' -                                                    -
' -        END MACRO CODE                              -
' -                                                    -
' ------------------------------------------------------
 


If you add the code above and recompile trans, you can write something like the following;


Class Action
  Method Do:Void() Abstract
End

Class Action1<T>
  Method Do:Void(item:T) Abstract
End

Class Action2<T,V>
  Method Do:Void(sender:T,value:V) Abstract
End

Class Func<T>
  Method Do:T() Abstract
End
Class Func1<T,R>
  Method Do:R(value:T) Abstract
End
Class Func2<T,V,R>
  Method Do:R(value:T,value2:V) Abstract
End
Class Func3<T,V,A,R>
  Method Do:R(value:T,value2:V,value3:A) Abstract
End
    
Local sl := New SpriteStack()

  sl.Push(New Circle(10,10,5))
  sl.Push(New Circle(20,10,5))
  sl.Push(New Oval(20,20,25,15))
  sl.Push(New Circle(30,20,15))
  sl.Push(New Oval(20,30,15,45))
  sl.Push(New Rect(20,20,15,15))
  sl.Push(New Rect(20,30,15,45))
  sl.Push(New Sprite(0,30))
  sl.Push(New Rect(20,20,25,15))
  sl.Push(New Sprite(20,10))
  sl.Push(New Sprite(50,30))
  sl.Push(New Oval(20,20,15,15))

  sl.Where({(x:Sprite)? => x.X>10}).ForEach {(x:Sprite)=>
    x.Draw()
  }

  Local ints := New bbIntList([1,2,3,4,5,6,7,8,9])

  Local test = 5
  Local evens := {(x)? => x > |test|}

  For Local i := Eachin ints.Where(evens)
    Print i
  End

  Print {(x)% => x*x}.Do(5)
  Local cube := {(x)# => x*x*x}

  Print cube.Do(5)


This is just a mockup for fun, but it's close to what the C# compiler does.


muddy_shoes(Posted 2014) [#2]
Nice work. It'd be great to see these sorts of features in Monkey.


maltic(Posted 2014) [#3]
I would love to seen lambdas in Monkey. The only problems I see are with closures, since every lambda will need to carry around a context, this would create a lot of garbage. This could be a big problem on the C++ GC that comes with Monkey--I expect that making the collector generational might help since most closures are short lived. The reason C# can get away with lambdas (see F#) is that .NET and Mono have quite advanced generational, compacting, concurrent garbage collectors. Monkey has a (comparatively) simple incremental mark and sweep garbage collector. Still, I would absolutely use this feature if it were implemented.


Nobuyuki(Posted 2014) [#4]
Restricting curly braces to anonymous functions is an interesting thought. The following line confuses me, though:
sl.Where({(x:Sprite)? => x.X>10}).ForEach {(x:Sprite)=>  x.Draw()}


It's using LINQ-like query filtering, which I thought was something separate from lambda expressions (even though it's a really awesome way to use them). Furthermore, I have no idea what ForEach is doing in there. When trying to figure it out, I stumbled on this article, which I guess predicted the confusion from someone like me: http://blogs.msdn.com/b/ericlippert/archive/2009/05/18/foreach-vs-foreach.aspx . I agree that having ForEach as a pseudo-method which takes a fun is pretty confusing.

Maybe more like this? (Note: scope of x is on the anonymous function's level, so Local o could be called x too if you really wanted)
For local o:Sprite = EachIn sl.Where( {(x:Sprite)? =>  x.X > 10} )
    o.Draw()
Next



Edit: Writing the return type as a suffix of the arguments of an anonymous function confused me at first, too. If you were assigning an anonymous function to a variable, it might be more consistent to allow the return type to be inferred from the variable's type, with syntax similar to a cast being used to "Force" the anonymous function's return type. Some potential examples:

Global MyFunc:Bool = {(x:Int) =>  x > 10}

Global MyFunc:= {Bool(x:Int) =>  x > 10}


Also I was thinking that the syntax could be a little more Monkey-like, and that means making certain operators resemble keywords, like this:
Global MyFunc:Bool = {(x:Int) Returns x > 10}


Could make it more readable, particularly if => is only used in this very specific instance!


AdamRedwoods(Posted 2014) [#5]
Also I was thinking that the syntax could be a little more Monkey-like, and that means making certain operators resemble keywords

Although this is an interesting project, i feel that introducing brackets and unusual assignment operators deviates from the original vision of the language.


Nobuyuki(Posted 2014) [#6]
@AdamRedwoods

When doing background research for my post, I looked at how several languages implemented lambda expressions, including the syntax of several functional languages, and have found many of them to be pretty confusing. The language that I feel Monkey most closely resembles, VB.NET, has a lambda syntax which would probably be a nightmare to parse -- There is no operator or keyword which separates an anonymous function from the body of that function; the compiler is simply supposed to "recognize" it by context. As a side effect of that, it's also not visually apparent on its face to a casual observer that the code's different from the rest of the program in terms of flow! To make matters worse, it re-uses existing keywords in a new way to describe the beginning of an anonymous function as a layer of syntactic sugar:
  Dim increment1 = Function(x) x + 1


As you can see, Function is re used with slightly different syntax, and the function body is just kinda sitting out there off to the side. In a longer line of code this could get lost. On the other hand, curly braces aren't used anywhere in Monkey right now, so this does a really good job of emphasizing the paradigm shift that lambda expressions bring. My biggest concern with the "original vision of the language", as you'd put it, is with how that's interpreted. Some people might think that lambda expressions aren't included in that vision at all.

I think they would fit perfectly fine, and we can do it in a way that makes Monkey stand out as a flagship example of how a BASIC dialect could do it right. My thoughts on the matter are that they just have to be done without introducing "operator symbol soup" -- the crap that seems to infect languages like C++, particularly the fetishization of opaque operator overloading functionality. (cout << "Hello World", anyone?)

That's why I suggested making a new reserved keyword instead of a symbol for this purpose. And brackets, I was okay with only because they'd be so easy for anyone who knows Monkey to recognize it as a lambda expression, something which goes against the normal "flow" of imperative style. It would probably be easier to maintain in trans, too, because there would be less contextual guessing scenarios. (Mark seemed to make a deliberate choice avoiding using context-sensitive keywords in things like EachIn, Logic operators vs bitwise, and etc. compared to VB -- which has a whole boatload of context-sensitive keywords like If, Is, and so on.)

What would you suggest in its place?


maltic(Posted 2014) [#7]
If you wanted to keep in the tradition of BASIC-like syntax (as well as being pithy), you could always pull a Python/Scheme:
map(my_array, Lambda:Int (x:Int) x + 1 End)
Or the slightly clearer Ruby-Python cross breed:
map(my_array, Lambda:Int (x:Int) Do x + 1 End)
Both are completely context free in the LL(1) sense.

Personally I don't think you can get /much/ better than Haskell for syntax
map myArray (\ x -> x + 1)
(which is actually fully generic and type safe) but that isn't really an option here.

ML-like languages, particularly F#, are also not bad these days. If you enjoy these features in C#, you should check out F#:
Array.map myArray (fun x -> x + 1)
Again this is fully generic and type safe.

Of course, nothing will ever beat Forth:
my_array [1 +] map

How is that for minimal syntax?

Edit: Sorry for the tangent. Language design is a bit of a hobby of mine.


Samah(Posted 2014) [#8]
http://en.wikipedia.org/wiki/Shakespeare_(programming_language)

A program of lambda example.

Romeo, a young man of strong resolve.
Macbeth, an odd man with a kind streak.

     Act I: Assumption that Macbeth remembers infinite things
	 
	 Scene I: The retrieval of Macbeth

[Enter Romeo and Macbeth]

Romeo:
 Recall your past lives!
 You are as brave as the sum of thyself and a hero!

Macbeth:
 Remember me.
 Let us return to scene I.

[Exeunt]