Live data from Hacker News

Racket – Lisp beyond Clojure

slides.com

111–120 of 179 posts

Re: Racket – Lisp beyond Clojure

#111
post #106

Earlier quoted context omitted.

I 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.

An 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 S…

Clojure 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))

Re: Racket – Lisp beyond Clojure

#112
post #107

Earlier quoted context omitted.

this 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 thinkin…

When 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 t…

Okay, but at that point you're talking about things that are way beyond the capabilities of an iterative loop. I think my point still stands - that implementing a tail recursion in place of a loop is not something you will have to pay for. Both structures will map to the same instructions.

Re: Racket – Lisp beyond Clojure

#113
post #16

I'd think Common Lisp is the Lisp beyond Clojure. Also, I just see slides that have (googleable) terms, is there a recording of this presentation? EDIT: see mkozlows' comment (THANKS!!!)

TBH, almost any Lisp is beyond Clojure. Don't get me wrong, Clojure is an amazing tool. While it addresses a very specific use case (functional programming on the JVM) that use case is common enough that it's a very useful tool. But when compared to other Lisps in a context where non-JVM toolsets are acceptable, Clojure leaves a lot to be desired.

But, when I'm spoiled with built in immutable persistent collections and awesome concurrency semantics, am I not also desiring those features in other lisps.

Re: Racket – Lisp beyond Clojure

#115
post #88

Earlier quoted context omitted.

> Badly designed functions can do less design damage per-function than badly designed languages per construct Really? When was the last time you successfully used a C library without reading the documentation?

Straw man. When was the last time you had to check the documentation for the parameter evaluation strategy of a C function call? I don't think it is a controversial statement than a badly designed macro can do more damage than a badly designed function. I believe that this is true, even normalizing for bad macro systems. This is not a statement against macros, so much as it is a statement to consider when making trad…

What would you cite as an example of a badly designed macro?

Re: Racket – Lisp beyond Clojure

#116
post #111
post #106

Earlier quoted context omitted.

An 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 S…

Clojure 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))

Yep, 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.

Re: Racket – Lisp beyond Clojure

#117

I find DSLs to be extraordinarily powerful for writing and working with my own programs. I find them painful when I'm trying to work with other people's programs. Making a DSL lets me construct a language to express my program that meshes with the way I think. Unfortunately, we all think differently, and what is intuitive and friendly for me is hostile and awkward for others. While reading someone else's program that…

Creating reusable functions and types and whatnot is also creating your own DSL, as another poster hinted at.

However the main problem with bad DSLs is that they aren't composable. Which really means users have to learn the right sequence of events and the right context in which certain actions function properly. People often complain about awkward symbols or unfamiliar constructs, but really, they are missing the forest from the trees.

This is why plain pure functions are always better, because they are composable and reusable, being the missing ingredient of many DSL authors.

Re: Racket – Lisp beyond Clojure

#118
post #111
post #106

Earlier quoted context omitted.

An 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 S…

Clojure 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))

Another thing to mention: having guarantee of TCO supports higher-level control-flow abstractions (capturing continuations), which is also an interesting outcome...

Re: Racket – Lisp beyond Clojure

#119
post #107

Earlier quoted context omitted.

When 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 t…

Okay, but at that point you're talking about things that are way beyond the capabilities of an iterative loop. I think my point still stands - that implementing a tail recursion in place of a loop is not something you will have to pay for. Both structures will map to the same instructions.

Yep, you're absolutely right. I guess that maybe if you care about inventing new kinds of loops (e.g. a DSL that wants to loop as a specialized kind of `fold` construct), there's more flexibility with TCO.

But Clojure shows that there's tons of mileage and composability that can happen by using runtime protocols and tasteful language-design decisions even without TCO.

Capturing continuations is another interesting theoretical outcome of TCO, although as various scheme implementations show: the approach to supporting TCO also imposes some performance constraints on call/cc.

Re: Racket – Lisp beyond Clojure

#120
post #84

Earlier quoted context omitted.

Haskell 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 encoun…

Also started with 61a in Scheme. How would you compare Scheme to Racket?

Imagine 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

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.

Post reply on HN