4 ms·
Unhappy with the current state of functional programming. I know I should't be. I have a lot of options. I went to school at UC Berkeley. My first language was
by jnordwick 10y ago
Unhappy with the current state of functional programming. I know I should't be. I have a lot of options.
I went to school at UC Berkeley. My first language was Scheme. Then Elisp. CLOS was in the first few too. I really didn't learn C or C++ until second semester.
A few months ago I had to pick up enough Scala to figure out what some code did in a project and I wasn't really happy with the language. I realize it is multi-paradigm, but the functional part just seems a little bolted on at times. The notation was clunky. Maybe it is because I prefer Lisp-ish and APL-ish languages. And the Scala docs aren't very friendly. I actually find it easier to write imperative code in Scala than functional code.
Clojure seems nice, but lacks a lot of things that you kind of want in a LISP (proper tail rec). Racket has very little market pen.
Maybe I'm just picky, but I don't see anything I really want to put time into.
- DigitalJack 10y agoI guess I don't understand what the big deal is with tail call optimization. Could someone give an example where it really shines and clojure's loop/recur just doesn't? If you are looking to put time into a programming language that is interesting in and of itself, I'd suggest Haskell.
- jnordwick 10y agoLooping is fine for imperative languages. It should be the norm for performance sensitive things since that is how the CPU works. It is just easier than trying to make a smart compiler turn your recursion into looping constructions. However, for functional languages you really want TCO to work properly so you can write your algorithms in a properly functional way. And you don't just want self-calls to work, but full mutual recursion (A calls B that calls A etc) to be properly optimized too.
- optforfon 10y agothe CPU is just doing a jump to an instruction at the end of each iteration. When you do tail call recursion you are doing just that. The assembly jumps to a tag and in recursion you jump to a function name, which is also effectively a tag. So it's not really about mapping better to the CPU instructions or anything like that The added benefit is that with tail call recursion you can have mutually recursive function (function A calls function B at the end and function B calls function A at the end), which isn't possible with a simple iterative loop, so the functional style is actually more powerful than an iterative approach and still maps directly to "the way the CPU works"
- jnordwick 10y agoYou are forgetting about the function prologue, epilogue, and the maintenance of the various pointers. It certainly isn't as simple as a jump as you imply. To do proper TCO most implementation resort to function trampolines and other self-modifying trickery to get similar to loop-like performance. Within the confines of the JVM, without doing extensive source analysis, I doubt there is even a way to do it.
- optforfon 10y agothis is outside my field of expertise. Can you give an example of a tail call that needs "trickery"? I'm not 100% on this, but when you have a tail call I don't think you have to remember the state of the stack frame. "function prologue, epilogue, and the maintenance of the various pointers" aren't god given rules - they're things dictated by something like the C ABI so you can resume the caller. So if you're thinking in terms of C ABI then yes, it's a compiler optimization, but in principle I think it's a zero cost abstraction (though I'd argue that the iterative loop is the actual abstraction) In tail call recursion you're ensured the caller will never resume! When you enter a tail-recursive function you save (as you normally would) a return pointer for the program counter and allocate registers/memory for your function arguments. At the end of the function you've done all the computation that that frame will ever do, so you are free to overwrite the argument variables when calling the next iteration/recursive-step. The return pointer doesn't need to be touched at all. Where is the complexity you're talking about?
- senex 10y agoWhen TCO is "lexically scoped" like loop/recur, the compiler can handle it. When HoF are involved, you may have a case where a procedure calls a procedure-parameter, which calls another, and another... Something about the runtime has to recognize this or else the compiler has to accept more constrains. See, for example, how Gambit-C implements tail calls by compiling modules to single C functions and how there is a trade off for procedure calls across module boundaries versus Chicken Scheme's approach to essentially garbage-collecting the call stack.
- DigitalJack 10y agoI will have to look for mutual recursion support (or lack thereof) in clojure. Clojure's loop/recur is a work-around for some issue with the JVM and lack of fine grained control over the stack. That said, It's not a simple loop, the keyword "loop" is effectively an anonymous function that gets called by the "recur" keyword, with parameters. An example: (loop [iter 1 acc 0] (if (> iter 10) (println acc) (recur (inc iter) (+ acc iter)))) I don't know if that is any different than other lisps or not. In case it's less less than clear, the bracketed portion after the "loop" keyword is an initial binding form (setting iter to 1, and acc to 0), and thereafter iter and acc are just considered parameters. For self recursion, that seems like a fair compromise. No help at all for mutual recursion though.
- dmichulke 10y agoI never had to use it but I recall there was a 'trampoline' fn for mutual recursion
- smnplk 10y agoExactly. I once read a great article [1] written by Martin Trojer I think, it was about recursion in Clojure. EDIT: [1] https://martintrojer.github.io/clojure/2011/11/20/tail-calls-in-f-clojure-and-scala https://martintrojer.github.io/clojure/2011/11/20/tail-calls...
- ufo 10y agoloop/recur is good for replacing structured loops but tail recursion is more general, since it can replace arbitrary gotos. One example is given by Guy Steele in "Why Object-Oriented Languages need Tail Calls". Basically, tail recursion lets you keep O(1) stack space even in the face of opaque method dispatching in OO code. https://eighty-twenty.org/2011/10/01/oo-tail-calls https://eighty-twenty.org/2011/10/01/oo-tail-calls Another required reading are the "Lambda the Ultimate" papers, if you haven't read them yet. The "GOTO" one is particularly relevant here. http://library.readscheme.org/page1.html http://library.readscheme.org/page1.html
- DigitalJack 10y agoThanks! These must be where the website/blog got its name. I actually thought that was what you were referring to at first.
- shiro 10y agoIt's a big deal since it's not really about optimization, but more about the overall coding style and thought process. With TCO, you start seeing functions as composable code blocks rather than something that is called and then return from. State machines are just mutually tail-calling functions---it's not that state machines are written so, but that in your mental model two are the same. And usually a program is humongous multi-layered state machine. Suppose you want to abstract the part of the loop. You just write a generic version that tail-calls user-passed function. Users can extend the loop freely, even interlacing two independent loops by mutually calling them (which can't be done with callback model without consuming stack frames). It's really a matter of style, but for those who get used to think in this way, not having guaranteed TCO is like writing code with one hand tied up in the back.
- senex 10y agoAn interesting thing about general tail recursion is that it's composable. Closure's loop/recur is a special case implementation of a common pattern. That's Ok, but it can't extend. Any higher order function in scheme can be parameterized with methods that should be tail recursive. No special language support needed. That's why for-each is a procedure instead of a special form in scheme. This allows libraries (like SFRI-1) or DSLs to be created that expect callers can conform by contract instead of by using special forms. Callers, in turn, can create abstractions over the libraries and DSLs that are equally general, instead of being strongly influenced by what they're built on. Clojure does a really nice job of creating reusable abstractions. From what I understand, tail-call optimization isn't possible in the JVM, so we have iterators and lazy evaluation first-class instead; which has other nice advantages for creating abstractions.
- macmac 10y agoClojure has both recur and trampoline which handle tail calls (in general not just with loop) and trampoline for corecursion: recur: (defn factorial ([n] (factorial n 1)) ([n acc] (if (= n 0) acc (recur (dec n) (* acc n))))) trampoline: (declare is-even?) (defn is-odd? [n] (if (zero? n) false #(is-even? (dec n)))) (defn is-even? [n] (if (zero? n) true #(is-odd? (dec n)))) (trampoline (is-odd? 10000))
- senex 10y agoYep, and the same can be implemented in any language that supports functions-as-objects (for example, javascript). It's a practical solution/workaround, but isn't quite composable/generalizable as first-class support for TCO (unless the community is willing to follow some convention like "TCO-like behavior is a 'good thing' and so libraries should try to support and interface compatible with `trampoline`". This is a little bit like `function` / yield in newer versions of Javascript. It's not TCO, but it is a strong community convention (with the assist of special syntax to enforce the semantic). Clojure is a great example of how practical/tasteful decisions by language author can create re-usable abstractions that are really powerful (even while at the same time working around limitations of the underlying technology). Some might also argue that TCO is "bad" because it's less debuggable (e.g. when an exception is thrown, the stack trace is gone). We can see the same phenomena with non-blocking IO, for example. So maybe it's a Good Thing to have "less general" semantics in a language (e.g. function, trampoline, and loop/recur may be good-enough while at the same time preserving debuggability). There are always tradeoffs. What's nice about general TCO is that the composibility of unrelated libraries/DSLs is more probable whereas with conventions like trampoline, DSL/library authors need to understand the advantages and design the DSL/library in a compatible way in advance. As Clojure shows with lazy sequences, this can absolutely work given enough community awareness of the convention.
- technomancy 10y ago> Could someone give an example where it really shines and clojure's loop/recur just doesn't? In practice you don't replace TCO with loop/recur; you replace it with lazy sequences. They both solve the "how to work on unbounded data while consuming bounded memory" problem even though at first glance loop/recur seems more similar. Most people who complain about not having TCO have not fully wrapped their heads around lazy sequences.
- sgeisenh 10y agoHave you tried learning an ML variant? SML and OCaml are great, and Haskell isn't too bad, either.
- jnordwick 10y agoYes. Years ago I learned OCaml (and I really didn't care for it. Seemed like a big mess of random stuff). I haven't touched it in years though. I used to learn a new language about every other year. My resume even used to be written in hand coded PostScript.
- tikhonj 10y agoHaskell and OCaml are both great. I've had a fair amount of experience with both of them as well as Racket and, while I rather liked Racket, I would definitely order them Haskell > OCaml > Racket. (I did 61A in Scheme too, coincidentally.) I'm using Haskell professionally now and, while there are still a few things I miss from Racket and even OCaml, Haskell delivers the best overall programming experience I've encountered.
- cbHXBY1D 10y agoAlso started with 61a in Scheme. How would you compare Scheme to Racket?
- tikhonj 10y agoImagine a superset of 61a Scheme with a more robust standard library, better tools and some cool facilities for domain-specific languages. It still feels like the Scheme you wrote while having some useful basic features like structs built in: (struct document (author title content)) (document "Me" "My Document" "Blarg") It's not strictly a superset of Scheme (for example, lists are immutable) but it feels like it. It's not like you usually used set-car! and set-cdr! in normal Scheme either! A lot of random small things like the syntax for importing modules is better. Then, if you dive in further, there are some cool packages like an optional static type system, a fancy contract system and some cool facilities for transforming Racket into another language (complete with new syntax). I saw people doing some really cool stuff with that. For example, Rosette[1] lets you write normal Scheme with a #lang rosette annotation on top. Then, instead of running your code directly, you can turn it into a compiler that produces a logic formula from your code. It redefines core language constructs like if to be symbolically evaluated. I'm working with a system that does something similar in Haskell and its implementation is a lot more awkward. [1]: https://docs.racket-lang.org/rosette-guide/index.html https://docs.racket-lang.org/rosette-guide/index.html If you're doing something really meta like that, Racket is great—easily one of the best options around. If you're just writing normal code, it's basically a marginally better Scheme which is fine. But not great. I worked on a compiler written in Racket without using any of the meta features and it was okay, better for the task than any other dynamic language, but it would have been better with a more functional language with a real type system. The standard library is certainly better than Scheme's but that's because Scheme is tiny; I was constantly missing functions I'm used to having in Haskell whereas, except for the metaprogramming features, there's very little I miss in the other direction. That's my take on it, anyhow. Obviously, other people disagree. If you're just comparing Racket against Scheme, you can mostly think of it as a superset that's a strict upgrade.
- thothamon 10y agoWell, of course you're limiting yourself to lisps (except Scala, about which I share your dislikes). How about F#, which is a very practical functional language with good tooling on all platforms? Or Ocaml, or Haskell?
- johncolanduoni 10y ago> good tooling on all platforms Eh... I've run into loads of trouble with F# and Mono, and with .NET Core you have to deal with platform fragmentation if you use any NuGet packages. Editors are pretty good across platforms now (as long as you stay far away from Xamarin Studio/Mono Develop), but I wouldn't call it "good" for non-Windows platforms.
- jnordwick 10y agoI don't use too much Windows and Mono sucks from all my previous attempts at using it so that kind of kills F# from what I hear. I tried OCaml (many) years ago, but it always seemed like a kitchen sink research language. I like well designed languages. Ones with purpose when they start out. Ones I can see a path for. Languages of accretions (Python, Ruby, Javascript, etc) don't interests me as much unless they are languages of production value (Java). Java start off as a language of purpose (Write-One Run-Anywhere) but soon became a language of accretion. Nobody cares about WORA anymore, server side Java is where it is at now. Maybe I'll just go back to my little corner of APL called KDB+/Q/K[1][2][3] and wait for a language that really piques my interest. [1]https://en.wikipedia.org/wiki/Kdb%2B https://en.wikipedia.org/wiki/Kdb%2B [2]https://en.wikipedia.org/wiki/Q_(programming_language_from_Kx_Systems) https://en.wikipedia.org/wiki/Q_(programming_language_from_K... [3]https://en.wikipedia.org/wiki/K_(programming_language) https://en.wikipedia.org/wiki/K_(programming_language)
- weavie 10y agoWhat about Standard ML? [http://www.smlnj.org/sml.html http://www.smlnj.org/sml.html] Much simpler than Scala, OCaml etc.. Many would say much better designed. It is the god father of many languages. There are several very good compilers available. [http://www.smlnj.org/ http://www.smlnj.org/] [http://mosml.org/ http://mosml.org/] [http://mlton.org/ http://mlton.org/]
- dmichulke 10y agoCan you elaborate a little on what you think lacks in Clojure? I ask because I've never been happier with any language (I came from Java/Peolog) and I rarely use loops (maybe one for every 2kloc, mostly because a reduce does it in most cases) so I don't know what else I might be missing without knowing it.
- justinhj 10y agoMost of the rough edges in Scala syntax are where it handles static typing around functional code and I think they've done a solid job achieving that. I'm from a lisp background and I loved learning Clojure, but for my day job I wouldn't give up Scala