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] |