Live data from Hacker News

A difference between Haskell and Common Lisp

chrisdone.com

161–170 of 203 posts

Re: A difference between Haskell and Common Lisp

#161
post #124

Earlier quoted context omitted.

That's just wrong. Clisp is the most popular lisp bearing the name, but the LISP, Lisp, or lisp family includes Scheme, and Clojure. if you just refer to Lisp, you may be referring to Clisp, the lisp family, or maybe even LISP 1.5, the lisp equivalent of V7 unix.

Clisp is an implementation of Common Lisp. Common Lisp includes a core of the original Lisp. Lisp programs from the 60s can either be run or ported to Common Lisp with little or no effort. Clojure is FULLY incompatible with any other Lisp dialect or Lisp derived language. Porting code means 'rewrite'.

Oh. Sorry. I was abbreviating Common Lisp. Whoops. Anyways, that's like saying that Go, Plan 9 C, D, Java, and Cyclone aren't in the C family, because you can't run ANSI C '99 on them and have it work. The Lisp family is diverse.

Re: A difference between Haskell and Common Lisp

#162
post #99

Earlier quoted context omitted.

R6RS was basically DOA, with only 3 or 4 implementers (Guile, Ikarus, Ypsilon, and (partially) Racket) actually going along with it. Most of the other implementers, most notably CHICKEN's Felix Winkelman, refused. R7RS is now split into a small core, and a large standard library for practical, as opposed to teaching, use, whose development STILL isn't done.

The R7RS homepage says something different: >This is the home page for R7RS, the Revised⁷ Report on the Algorithmic Language Scheme. This version of Scheme has been divided into a small language, suitable for educators, researchers, and users of embedded languages; and a large language focused on the practical needs of mainstream software development. >The report on the small language was finalized on July 6, 2013. I…

Yeah, that's what I said. Large still isn't done.

Re: A difference between Haskell and Common Lisp

#163
post #158
post #157

Earlier quoted context omitted.

> No, I'm talking about delimited continuations. No need for TCO. The continuation contains only a portion of the stack (off the top). I don't think that resolves it. It's quite normal to want to chain together a few thousand or million monadic operations even within a single method (e.g. making a number of network calls, writing lines if you're managing I/O monadically). If doing that results in a stack overflow the…

> If doing that results in a stack overflow then that severely limits the utility of this approach. There is no stack overflow. A chaining of monads translate to a blocking of the continuation. The familiar imperative sequence of blocking operations is an instance of the continuation approach, and it isn't susceptible to stack overflows. > I find threads really hard to reason about, so "it's as easy as threads" doesn…

> There is no stack overflow. A chaining of monads translate to a blocking of the continuation. The familiar imperative sequence of blocking operations is an instance of the continuation approach, and it isn't susceptible to stack overflows.

So wait, you are doing TCO? When my code "unblocks" it will look like it's continued directly from the previous line, when actually the previous line called into some other code which then called into a continuation for the "tail" of this method, right?

> When you block a kernel thread, the OS takes care of all the continuation stuff -- but it does exactly that (the OS does run your single-threaded program in a continuation). Just as the OS mechanics of continuations don't get in the way of your understanding of the above code, language-level implementation of continuations shouldn't either. It is completely transparent to you -- your thread blocks, some handler does something, and then your thread resumes.

If it's the kind of thing that can be implemented transparently then I don't think I'd want to be managing it at all (and automatic CPS-ization is a valid alternative to threading (indeed as you say threading can be viewed as just a crude form of it) that can have performance advantages, but if it looks the same at the code level then it's "just" a performance optimization). The point of the monadic style is for if I do want to manage the sequencing explicitly.

More concretely I think the big advantage is that a monadic effect can be captured as a value. So if I want to e.g. perform some effectful operation on a list, and I don't know about traverse, I can just do the operation in the normal way with map, and then as long as I use those values in a way that makes sense according to the normal rules of the language then I'll do something that makes sense. You draw an analogy with checked exceptions in Java - but most Java developers avoid using them, regard them as a failure, and I think a big part of that is you can't safely factor out the result of a call to a possibly-exception-throwing method as a value.

> Well, it's sort of my made-up term that I use because "functional" doesn't really have a definition (try to come up with a definition that encompasses all of: Lisp, ML, Haskell, and Erlang but none of Java, Ruby, Smalltalk, JS and you'll see why), so I use the term to denote languages which we call functional (whatever exactly that means) which aren't pure, so: the Lisps, MLs, Erlang.

Do you just mean non-Haskell languages?

I don't think "pure" is binary in that sense: all languages have some things that are managed implicitly by the compiler/runtime/infrastructure and some things that the developer is expected to manage implicitly. Haskell explicitly sequences I/O but the infrastructure manages laziness, whereas in most MLs it's the other way around. Which is more pure is a matter of perspective.

Re: A difference between Haskell and Common Lisp

#164
post #114

Earlier quoted context omitted.

UNIX may have been about small commands ("cat -v", anybody?), but its most important property is COMPOSABILITY. You could use small programs to build larger programs, allowing you to do things by gluing together code that you would previously have to write new programs to do. It made this very easy, and you could also do it from within C, so it wasn't an either/or situation. This is something your namesake system cou…

That was already available in Xerox PARC systems, by making use of function call composition in Interlisp-D REPL, Builders in Smalltalk transcript or live debugger in Mesa/Cedar. The UNIX composition is only a novelty for those that never saw other OSes that surfaced at the same time. After all UNIX just adopted the idea from MULTICS.

But pipelines made it convenient, and the shell made it language-agnostic. One or the other was lacking in your other examples.

Re: A difference between Haskell and Common Lisp

#165

Earlier quoted context omitted.

unix commands may not be the pinnacle of minimalism, but the most important thing UNIX taught is about composability. Yes, I did recognize the irony of the fact that the common lisp example was much more like a unix command line than the haskell example, but if you're talking about composing (relatively) small commands, unix is still a pretty good comparison.

For being a pinnacle it is really unsatisfying. It tells you very little about how or what programs can be composed. It's either trial and error or reading man pages. In addition every program has to be written to take in anything as there are no constraints. Function composition via types is a much more satisfying take on this problem. It's too bad Unix is regarded as the holy grail of this technique when it barely…

I never said it was the pinnacle of composition of programs. I did say it popularized it.

>Text just isn't a great medium for IPC.

Yeah, text does suck for IPC. The problem is, everything else sucks even more.

Re: A difference between Haskell and Common Lisp

#166
post #163
post #158

Earlier quoted context omitted.

> If doing that results in a stack overflow then that severely limits the utility of this approach. There is no stack overflow. A chaining of monads translate to a blocking of the continuation. The familiar imperative sequence of blocking operations is an instance of the continuation approach, and it isn't susceptible to stack overflows. > I find threads really hard to reason about, so "it's as easy as threads" doesn…

> There is no stack overflow. A chaining of monads translate to a blocking of the continuation. The familiar imperative sequence of blocking operations is an instance of the continuation approach, and it isn't susceptible to stack overflows. So wait, you are doing TCO? When my code "unblocks" it will look like it's continued directly from the previous line, when actually the previous line called into some other code…

> So wait, you are doing TCO? When my code "unblocks" it will look like it's continued directly from the previous line, when actually the previous line called into some other code which then called into a continuation for the "tail" of this method, right?

No TCO. Your code doesn't "unblock"; it unblocks (no quotes). The handler code doesn't "call into the continuation" (i.e., there is no stack-growing call), it just resumes the continuation. Like unparking a thread (or not "like"; unparking a thread is resuming a continuation). There is a relationship between continuations and TCO in that they both deal with stacks, but in this case it only serves to confuse.

> The point of the monadic style is for if I do want to manage the sequencing explicitly.

But how is `a = read(); print(a)` not explicit?

> More concretely I think the big advantage is that a monadic effect can be captured as a value.

Continuations are the same in that they are also a "value", except that it is a mutable value. A continuation has a state (it's current program counter). The calls to read and print trigger an OS handler. When working with continuations, those are programmable handlers, so their function is pluggable. You can run the continuation calling read and print with a handler that actually does IO, or with one that does something else -- just like with monads.

> You draw an analogy with checked exceptions in Java

That's only to explain the typing. Java's checked continuations are an effect system, and the list of thrown exceptions can be viewed as a list of effects a function may have. That's how you can ensure that a function calling read and print will only be called within a context that implements a handler for those effects.

> most Java developers avoid using them, regard them as a failure

Off-topic, but that is a myth (there was an unscientific poll about this in the last JavaOne conference). If there is a failure is not with the concept, but that the standard Java libraries overuse checked exceptions (this may be addressed). Most Java developers like the concept (according to the poll).

> and I think a big part of that is you can't safely factor out the result of a call to a possibly-exception-throwing method as a value.

Don't think of checked exceptions in this context as actual exceptions, but as a list of effects. The same "problem" applies to monads, and is actually a good thing -- as you said yourself -- to have the type system ensure that monadic methods are called within the appropriate monad only. This kind of effect system also ensures that a function with effects is only called when the needed handlers are in context. The difference between continuation-effects and monads is that a function making use of continuations can use multiple effects without requiring nasty monad-transformers (and that's exactly the problem Kiselyov wanted to solve with his handler system).

> Do you just mean non-Haskell languages?

Basically yes (but Idris, Agda and Coq are also pure).

> I don't think "pure" is binary in that sense: all languages have some things that are managed implicitly by the compiler/runtime/infrastructure and some things that the developer is expected to manage implicitly.

I certainly agree in theory: what constitutes an effect (except IO) is indeed up to the language to define, but those languages I listed allow all effects (including IO, which is an "absolute" effect) without declaring them explicitly, hence they are not pure.

BTW, there is another absolute effect (i.e. an effect which is externally observable, and thus must be considered an effect regardless of the language's preferences) -- time. Haskell (and Idris etc.) don't have a good mechanism of handling that effect, which is one of the reasons I am not fond of the Haskell-style pure-FP.

As a nice demonstration of that problem, there are two ways in Haskell to write a function that sleeps for a second: one that is effectful (monadic) and one that is pure. Also, many people believe that Haskell has only one function inhabiting the type forall a: a -> a (a notion that is at the core of Haskell's equationality myth), when in fact there are infinitely many, all different from one another.

Re: A difference between Haskell and Common Lisp

#167
post #114

Earlier quoted context omitted.

That was already available in Xerox PARC systems, by making use of function call composition in Interlisp-D REPL, Builders in Smalltalk transcript or live debugger in Mesa/Cedar. The UNIX composition is only a novelty for those that never saw other OSes that surfaced at the same time. After all UNIX just adopted the idea from MULTICS.

"The UNIX composition is only a novelty for those that never saw other OSes that surfaced at the same time. After all UNIX just adopted the idea from MULTICS." One could certainly chain programs through intermediate files. My understanding has it that pipelines were new in Unix, and Wikipedia agrees: "The pipeline concept was invented by Douglas McIlroy and first described in the man pages of Version 3 Unix."

Yes the concept was popularized by UNIX, but anyone that spends time doing computer archeology will find similar patterns in other OSes, like the ones I listed.

Don't forget that back then computing was developed in silos, with knowledge only shared at conferences or when researchers switched universities/companies.

So it was quite common that researchers across the globe would come up with similar discoveries.

Re: A difference between Haskell and Common Lisp

#168
post #87
post #79

Earlier quoted context omitted.

> You could pass around explicit dictionaries with all your values or some such, … if you have an extremely low-overhead way of doing so A lot of that plumbing could be hidden from the user given the right dynamic features. http://www.eighty-twenty.org/2015/01/25/monads-in-dynamicall...

It sounds like racket generics are at least partway on the road to a type system. And those placeholder values seem a bit greenspunny - I'm not sure how they'd interact with native racket features (e.g. macros). (I mean, you are right, but in a degenerate sense you could implement typeclasses in any dynamic language by having your program construct strings and writing a Haskell compiler in that language that executed…

The big difference is that generic dispatch is resolved dynamically.

I haven't yet found myself reaching for monads in Racket (or writing much non-Redex Racket code) since tonyg wrote that post, so I don't know if there are any odd interactions to watch out for.

Re: A difference between Haskell and Common Lisp

#169
post #166
post #163

Earlier quoted context omitted.

> There is no stack overflow. A chaining of monads translate to a blocking of the continuation. The familiar imperative sequence of blocking operations is an instance of the continuation approach, and it isn't susceptible to stack overflows. So wait, you are doing TCO? When my code "unblocks" it will look like it's continued directly from the previous line, when actually the previous line called into some other code…

> So wait, you are doing TCO? When my code "unblocks" it will look like it's continued directly from the previous line, when actually the previous line called into some other code which then called into a continuation for the "tail" of this method, right? No TCO. Your code doesn't "unblock"; it unblocks (no quotes). The handler code doesn't "call into the continuation" (i.e., there is no stack-growing call), it just…

> But how is `a = read(); print(a)` not explicit?

It doesn't tell me which of the calls is effectful and which is pure. It's clear enough at the direct level, but if I'm calling methods that I don't know the implementation of then the =/> That's only to explain the typing. Java's checked continuations are an effect system, and the list of thrown exceptions can be viewed as a list of effects a function may have. That's how you can ensure that a function calling read and print will only be called within a context that implements a handler for those effects.

So if I do "a = read()" what is the type of a? "(String effects IO)" or some such? If it's "string, but only allowed inside the scope of a method with IO as an effect" then it's the same problem as checked exceptions.

> Off-topic, but that is a myth (there was an unscientific poll about this in the last JavaOne conference).

Hmm. I wouldn't expect conference-goers to be a representative sample, and it doesn't match my experience.

> The difference between continuation-effects and monads is that a function making use of continuations can use multiple effects without requiring nasty monad-transformers (and that's exactly the problem Kiselyov wanted to solve with his handler system).

Since you're emphasizing it I'll ask: how do you express effects that don't commute? EitherT[Writer[Log, ?], Error, Value] behaves very differently from WriterT[Either[Error, ?], Log, Value].

Re: A difference between Haskell and Common Lisp

#170
post #169
post #166

Earlier quoted context omitted.

> So wait, you are doing TCO? When my code "unblocks" it will look like it's continued directly from the previous line, when actually the previous line called into some other code which then called into a continuation for the "tail" of this method, right? No TCO. Your code doesn't "unblock"; it unblocks (no quotes). The handler code doesn't "call into the continuation" (i.e., there is no stack-growing call), it just…

> But how is `a = read(); print(a)` not explicit? It doesn't tell me which of the calls is effectful and which is pure. It's clear enough at the direct level, but if I'm calling methods that I don't know the implementation of then the =/ > That's only to explain the typing. Java's checked continuations are an effect system, and the list of thrown exceptions can be viewed as a list of effects a function may have. That…

> It doesn't tell me which of the calls is effectful and which is pure.

But it does because their type says so:

   String read() effect IO
(where `effect` is just like Java's checked `throws`)

> what is the type of a? "(String effects IO)" or some such?

Right. It is as I said, a type on read, not on a. `a` is still `String`.

> only allowed inside the scope of a method with IO as an effect then it's the same problem as checked exceptions.

But that's the opposite of a problem. That's like using `IO a` outside of an IO monad. You want the function to only be called in the right context (an appropriate handler is in effect).

> it doesn't match my experience.

Fair enough, and you're not alone, but here's the relevant discussion (about 4 minutes): https://youtu.be/iHHSa39p48I?t=8h14m11s

> Since you're emphasizing it I'll ask: how do you express effects that don't commute?

The same way:

    void foo() effects Either>
etc.. (this doesn't work with Java's current type system, which doesn't allow generic exception types, though).

I'm not sure if the same effects would make sense, though. For example, Either is a monadic value which doesn't make sense in an imperative setting.

This works because you can have:

     T check(T x) effects MyEffect
Post reply on HN