C# like lambdas and closures
Monkey Forums/Monkey Code/C# like lambdas and closures
| ||
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. |
| ||
| Nice work. It'd be great to see these sorts of features in Monkey. |
| ||
| 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. |
| ||
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! |
| ||
| 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. |
| ||
| @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? |
| ||
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. |
| ||
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] |