Live data from Hacker News

Why I still reach for Lisp and Scheme instead of Haskell

jointhefreeworld.org

161–170 of 172 posts

Re: Why I still reach for Lisp and Scheme instead of Haskell

#161
post #32

Seems a bit similar to 'Why I prefer Scheme to Haskell' ( https://news.ycombinator.com/item?id=3816385 >, 2012). Seems a bit plagiarized, but that may just be a coincidence.

It is odd. Especially since the author of the article states they’ve been working with Haskell for years, although they only have a few years of experience and only have a single Haskell repository on GitHub with some simple leetcode problems.

The number of both nearly and exactly identical phrasing makes it feel like someone told an LLM to slightly rephrase that older article.

I didn’t even know the internet was sick.

Re: Why I still reach for Lisp and Scheme instead of Haskell

#162

Earlier quoted context omitted.

Operatives do that for me, better than macros. Parent is correct that macros are compile time, which gives them a performance advantage over operatives - but IMO, they're not better ergonomically. I find operatives simpler, cleaner and more powerful.

I hadn't heard of operatives. Can you describe them or provide a link?

Operatives are based on FEXPRS from older lisps - they're basically a function-like form, but where the operands are not implicitly reduce at the time of call.

    (foo (+ 2 3) (* 3 4))
    ($bar (+ 2 3) (* 3 4))
`foo` is a function, when it is combined with the arguments, it receives the values 5 and 7.

`$bar` however, receives its operands verbatim. It receives (+ 2 3) as its first operand and (* 3 4) as its second - unevaluated.

The operative/FEXPR body decides how to evaluate the operands - if at all.

The difference between an operative/FEXPR and a macro is that macros are second-class objects which must appear in their own name - we cannot assign them to variables, pass them or return them from functions. Operatives and FEXPRs are first-class objects that can be treated like any other.

The difference between FEXPRs and Operatives is to do with scoping and environments. FEXPRs were around before Scheme - when Lisps were dynamically scoped. This meant we could have unpredictable behavior and so called "spooky action at distance". They were problematic and basically abandoned almost entirely in the 1980s.

Shutt introduced Operatives as a more hygienic version - based on statically scoped Scheme. Instead of the operative being able to mutate the dynamic environment arbitrarily, there are limitations. The first part of this is that environments are made into first-class objects - so we can assign them to a symbol and pass them around. The final part is that an operative receives a reference to the dynamic environment of its caller - which we bind to a symbol using the operative constructor, `$vau`.

    ($vau (operands) dynamic-env . body)
Compare to:

    ($lambda (arguments) . body)
So operatives are called in the same way a function is called - but the operands are not reduced, and the environment is passed implicitly.

The body can decide to evaluate the operands using the environment of the caller - essentially behaving as if the caller had evaluated them

    (eval operands dynamic-env)

But it can chose other evaluation strategies for the operands - such as evaluating them in a custom created environment which we can make with (make-environment) or ($bindings->environment).

This also allows the operative to mutate the environment of its callee - but only the locals of that environment. The parent environments cannot be mutated through the reference `dynamic-env`.

Technically, `$lambda` is not primitive in Kernel - though it is the main constructor of applicatives (functions) - the primitive constructor is called `wrap` - and it takes another combiner (an operative or applicative) as its parameter. Wrapping a combiner simply forces the evaluation of its arguments when called - so functions are just wrappers around operatives - and the underlying operative of any function can be extracted with `unwrap`.

There's a lot more to them. They're conceptually quite simple in terms of implementation, but they have enormous potential use cases that are unexplored.

Read more on the Kernel page[1]. In particular, the Kernel report[2]. There's also a formal calculus describing them, called the vau calculus[3].

[1]:https://web.cs.wpi.edu/~jshutt/kernel.html

[2]:https://ftp.cs.wpi.edu/pub/techreports/pdf/05-07.pdf

[3]:https://web.archive.org/web/20150224035948/http://www.wpi.ed...

Re: Why I still reach for Lisp and Scheme instead of Haskell

#163

> Actually, in my opinion, Scheme (and Lisp) allows you to express complex systems and problem domains in more simple terms than any other language can. I disagree with this. Lisps are procedural languages like most other programming languages. They can describe procedures in a relatively simple manner (big asterisk on that one, the simplicity of Lisp as a language is greatly overstated and conflated with the simplic…

Lisps are expression based languages, but not pure. It's easy to mistake it as "like most other languages", but it's not quite the same - everything is an expression and returns a result. There are no "statements".

They appear procedural because of syntax sugar - ie, the body of a function is basically implicitly wrapped in (progn ...), (begin ...), ($sequence ...), etc - which are all equivalent expression forms which evaluate their sub-expressions in order and return the result of the last one.

   (progn a b c)      ;; CommonLisp
   (begin a b c)      ;; Scheme
   ($sequence a b c)  ;; Kernel

   ;; evaluate a, then b, then c, 
   ;; ignore the results of evaluating a and b 
   ;; return the result of evaluating c.
So when you see:

   (define (foo)
       (expr1)
       (expr2)
       (expr3))
If we desugar, it would be

    (define foo (lambda () (begin (expr1) (expr3) (expr3))))
We get behavior that looks just like other procedural languages (without a "return" keyword) - but everything is still an expression.

A similarity is the comma operator in C. Imagine you didn't write statements but the body of your C functions was entirely chains of comma operators.

CommonLisp has a couple of other useful related forms - prog1 and prog2. They still evaluate their sub-expressions in order, but prog1 returns the result of evaluating the first expression, and prog2 returns the result of the second expression.

    (define (foo) (prog1 (expr1) (expr2) (expr3))
    (foo)
 
    ;; evaluates expr1, then expr2, then expr3
    ;; returns the result of evaluating expr1
    ;; ignores the results of evaluating expr2 and expr3

Re: Why I still reach for Lisp and Scheme instead of Haskell

#164
post #128

Earlier quoted context omitted.

What makes you think it falls flat in a team setting? There are plenty of N-pizza-sized teams successfully using Lisp to this day and you're probably aware of many teams successfully using Lisp in the past, too. There's also the success of Clojure. What's required to have a well functioning team is mostly programming language independent; Lisp itself won't save a team lacking those properties anymore than say Java wo…

Did you even read what I said or who I responded to? I am specifically talking about working inside an image, monkey patching functions and structures live in the running image. A practice almost no one uses anymore and of which I said that as a single dev on a project I use and find convenient, but I would not want to use it in a team; for that, modern workflows with versioning, beaming code, ci/cd, dev containers e…

I did, I still ended up pattern matching on too broad an interpretation of "it". Thanks for clarifying.

Re: Why I still reach for Lisp and Scheme instead of Haskell

#165

Earlier quoted context omitted.

You know, after some testing with a bunch of different scheme implementations, I take back what I said, at least for working in a REPL. (define (displayln msg) (display msg) (newline)) (define (inner x) (+ x 1)) (define (outer x) (inner x)) (displayln (outer 5)) (define (inner x) (+ x 2)) (displayln (outer 5)) outputs 6 and 7 in every one I tried, not the 6 and 6 I expected.

Perhaps you were thinking of lexical scope vs dynamic scope? Lexical scoping would prevent a local definition of inner from changing the definition used in outer. (let ((inner (lambda (x) (+ x 3)))) (outer 5)) "7" But updating the definition of inner with set! or define changes the top-level definition.

Yes, I was thinking of lexical scope; as in redefining the function shadows the existing one and doesn't change the function already defined uses of call.

Re: Why I still reach for Lisp and Scheme instead of Haskell

#166

Earlier quoted context omitted.

I hadn't heard of operatives. Can you describe them or provide a link?

Operatives are based on FEXPRS from older lisps - they're basically a function-like form, but where the operands are not implicitly reduce at the time of call. (foo (+ 2 3) (* 3 4)) ($bar (+ 2 3) (* 3 4)) `foo` is a function, when it is combined with the arguments , it receives the values 5 and 7. `$bar` however, receives its operands verbatim. It receives (+ 2 3) as its first operand and (* 3 4) as its second - unev…

Hmm, this sounds like exactly the opposite of what I was talking about. It delays execution rather than promoting execution to compile time.

What I had expected you to talk about was some way of getting the compile time execution of macros by a sufficiently smart compiler that could do extensive partial evaluation at compile time, including crossing procedure boundaries. Of course that's antithetical to the Lisp philosophy of allowing dynamic redefinition of functions and such.

Re: Why I still reach for Lisp and Scheme instead of Haskell

#167

> Actually, in my opinion, Scheme (and Lisp) allows you to express complex systems and problem domains in more simple terms than any other language can. I disagree with this. Lisps are procedural languages like most other programming languages. They can describe procedures in a relatively simple manner (big asterisk on that one, the simplicity of Lisp as a language is greatly overstated and conflated with the simplic…

Lisps are expression based languages, but not pure. It's easy to mistake it as "like most other languages", but it's not quite the same - everything is an expression and returns a result. There are no "statements". They appear procedural because of syntax sugar - ie, the body of a function is basically implicitly wrapped in (progn ...), (begin ...), ($sequence ...), etc - which are all equivalent expression forms whi…

I'm very familiar with Lisp, you don't have to explain it to me. I think you are mistaken as to the meaning of what is meant by "procedural language" here. It's simply the mode of computation where a program is directly conceived as a hierarchical sequence of steps. I think you got caught up in the idea of a grand disjunction of "expressions" and "statements", with a distinguishing feature involving return values, and so on. But no, that's not particularly relevant here (nor is it universally true, or applicable)

To simplify it, you can consider cons, car, cdr the beating heart of Lisp. These special forms directly encode the execution semantics as the traversal of a head over cells of a tape. Lisp belongs to the same family as the Turing machine, and that's a very big family. SML also belongs to this family. The overwhelming majority of programming languages belong to this family.

Re: Why I still reach for Lisp and Scheme instead of Haskell

#168
post #164

Earlier quoted context omitted.

Did you even read what I said or who I responded to? I am specifically talking about working inside an image, monkey patching functions and structures live in the running image. A practice almost no one uses anymore and of which I said that as a single dev on a project I use and find convenient, but I would not want to use it in a team; for that, modern workflows with versioning, beaming code, ci/cd, dev containers e…

I did, I still ended up pattern matching on too broad an interpretation of "it". Thanks for clarifying.

Keep on evangelising CL though so will I!

Re: Why I still reach for Lisp and Scheme instead of Haskell

#169
post #64

Earlier quoted context omitted.

This is doable in Common Lisp, Scheme/Racket, and Clojure. Yes, it might require some tooling.

Can you elaborate on how this is doable (in, say, Racket) and what tooling is needed? I'm afraid your reply doesn't add much information beyond the same assertion that I quoted that was in the article posted to HN. And I haven't been able to find information on this with Racket.

Emacs+Geiser, which does the same with Slime against Common Lisp languages.

Re: Why I still reach for Lisp and Scheme instead of Haskell

#170
post #118

Earlier quoted context omitted.

That's fine for a library or locally run executable, but I've worked on distributed systems in Haskell and you really need logging in place to track what is going on. Of course, you will have IO somewhere in a executable where you can handle logging so just separate pure and IO and make sure you have good tests for the pure functions. Also, linting to catch partial functions and dangerous lazy ones (or use an alterna…

Sure you want logging and tracing (in the RPC sense not Debug.Trace.trace). Most of this can still be done from IO places where the pure functions collect enough error information bubbling up (e.g. content and line/col of parser errors etc.) to not need ad hoc print statements for debugging.

Yes, definitely wouldn't use print because in an async environment that will get garbled. A proper logging library is the way to go.
Post reply on HN