
A Haskell Programmer Tries to Learn Racket - jackhammer2022
http://artyom.me/learning-racket-1
======
gaigepr
"...but nothing beyond that. As long as Node.js exists in this world, I can't
truly hate anything else."

I found this hilarious. I am also rather underwhelmed (to be nice) with Nodejs
and a little bothered at its wide adoption.

I have also been learning racket recently; my formal language and functional
programming class uses it. I had some previous experience with common lisp but
the raw nature of scheme still pleasantly surprised me a little bit.

EDIT: From what I remember, javascript was "inspired" by scheme. Obviously
that when well...

~~~
MichaelGG
I keep flipping back and forth, thinking I don't get it, then thinking others
don't get it. Single-threaded JS with callbacks. OK? What am I missing?

The only thing Node has going for it is a lot of networking libraries. It's
not fast, the async style leads to callback hell, JS is a terrible language,
and their package management system is slow and bloated (or at least the
packages are)[1]. Yet people think it's some new kind of thing that's just so
awesome, despite MS doing server-side JS in the mid-90s.

Sure, people can use whatever makes them happy. I'm just confused as to why
anyone is particularly impressed with Node or JS.

1: Seriously, to automate builds, I ended up saving the node_modules directory
then symlinking it in to the build environment. Otherwise a simple CSS/JS/etc.
grunt build took 15 minutes instead of 1. And it seems every module pulls in
its own copy of dependencies, so you end up with 300-character deep paths.
There's no apparent reason it needs to be like this.

~~~
ludwigvan
Straight from the horse's mouth:

"Node is popular because it allows normal people to do high concurrency
servers. It's not the fastest or leanest or even very well put together - but
it makes good trade offs in terms of cognitive overhead, simplicity of
implementation, and performance. I have a lot of problems with Node myself -
but the single event loop per process is not one of them. I think that is a
good programming model for app developers. I love Go so much (SO MUCH), but I
cannot get past the fact that goroutines share memory or that it's statically
typed. I love Erlang but I cannot get the past the syntax. I do not like the
JVM because it takes too long to startup and has a bad history of XML files
and IDE integration - which give me a bad vibe. Maybe you don't care about
Erlang's syntax or static typing but this is probably because you're looking
at it from the perspective of an engineer trying to find a good way to
implement your website today. This is the source of our misunderstanding - I
am not an app programmer arguing what the best platform to use for my website
--I'm a systems person attempting to make programming better. Syntax and
overall vibe are important to me. I want programming computers to be like
coloring with crayons and playing with duplo blocks. If my job was keeping
Twitter up, of course I'd using a robust technology like the JVM. Node's
problem is that some of its users want to use it for everything? So what? I
have no interest in educating people to be well-rounded pragmatic server
engineers, that's Tim O'Reilly's job (or maybe it's your job?). I just want to
make computers suck less. Node has a large number of newbie programmers. I'm
proud of that; I want to make things that lots of people use. The future of
server programming does not have parallel access to shared memory. I am not
concerned about serialization overhead for message passing between threads
because I do not think it's the bottleneck for real programs."
[https://news.ycombinator.com/item?id=4310723](https://news.ycombinator.com/item?id=4310723)

~~~
rdtsc
> The future of server programming does not have parallel access to shared
> memory.

Yeah but it could be IO parallelism. There could be two instances of callback
chains of sequence C1->C2->C3 started such that the the second starts before
the first one finished. As in C1->C2 ran then C1 gets called again. If in
those callbacks you update a data structure (a database record?), you now
accessed that data in parallel. So you have to protect against that with some
kind of a lock/mutex. Yeah context switching doesn't potentially happen at
every assembly instruction, the granularity is much higher, but it is still
there.

~~~
dllthomas
There's some need to synchronize, but hot damn is it simpler when you're
dealing with sensible blocks of high-level statements than when you're dealing
with out-of-order parts of assembly instructions.

~~~
swift
That's true, but personally I still don't find this model ideal because the
synchronization points are implicit. The cleanest concept I know of for
dealing with synchronization is transactions.

------
nnq
This sums up quite a lot about Lisps in general. I'm amazed OP got so fast to
this "insight" :)

    
    
        (And this is probably Lisp's greatest weakness as well – with this level of
        possible diversity, everyone has to use the “common lowest denominator”
        simply because nobody can agree on what alternative syntax / library / etc.
        is better and should be used.)
    
        Off-topic: it's not enough to give everyone opportunity to improve the
        language; you have to choose the winners and promote them heavily. The rules
        of free market don't work here; people won't use the best thing, they'll use
        the one which is available out of the box and which is used by their peers.

~~~
Dewie
I dream of a modular language in which languages, or _dialects_ , can be built
from a small base language, which can then be extended, and so on. Of the
languages that I've seen, Racket looks the most promising. Forth might be good
for this, too, but to build large hierarchies of languages seems the
antithesis of Forth - or at least, Chuck Moore's - philosophy.

On the other hand, such a language might just end up as an incredibly
fragmented mess of different languages, with little interoperability between
languages - some one makes a 'typed' version of the language, another takes
that version and tunes it just enough for it to be incompatible with the
original version and, in turn, all languages that are built on top of that
original typed language, and so on.

Maybe incredible modularity is fundamentally at odds with (social)
interoperability.

~~~
dmytrish
It's ironic that Haskell _is_ such a language: it grows from a tight, small
core into larger and greater abilities as you learn it even without using its
extensions.

I agree that Common Lisp a very powerful language, but I can't live with all
that power uncontrollably thrown on me. Common Lisp grossly lacks self-
discipline and self-limitation when it's needed.

~~~
sparkie
I suspect the parent thread was dreaming of a framework where languages with
arbitrary syntax can be mixed and matched in user-defined ways.

Haskell isn't necessarily that language, partly because it still requires
centralized coordination of development of these "extensions" to ensure
they're interoperable - that is, there is only one _parser_ for the language
and its supported extensions, and many of them are build into the compiler
rather than added as libraries, except those extensions which are done through
quasi-quotation, such as with MetaHaskell, or some EDSL. Even that has it's
own problems, and you'll have issues parsing if your quoted language happens
to have delimiters which conflict with Haskell's quasiquoting delimiters `[|
|]` - producing syntax which cannot be parsed unambiguously (perhaps very rare
or unlikely though).

Perhaps the biggest hurdle of having a modular language is that we do not
understand how unambiguously parse the combination of two or more syntaxes. We
only know that composition of two CFGs results in another CFG, but with no
guarantee of unambiguity, and other parsers such as PEG rely on ordered
choice, where the computer can't decide which choice you really want.

What makes lisps great for composition of languages (or "EDSLs" in market
speak), is that it bypasses the parsing problem by asking you to just write
your language directly in terms of the syntax tree which a parser would
generate - and perhaps use macros or other functions to simplify the use of
that tree. Instead of a language being vocabulary+syntax, we create new
vocabulary for what would be done through syntax in other languages - and we
can thus refer to it unambiguously. Similar can be done in haskell too,
through regular functions and quotation.

The parse problem is only really a problem because we're stuck with this silly
model of "sequential text files" to describe code, and we're required to limit
our languages such that a parser can take one of these text files and make
sense of it. When we break out of this model, and use intelligent editors, we
can reach the point where syntaxes can be composed arbitrarily, because we can
indicate where each new syntax begins and ends. Diekmann and Tratt have
demonstrated how this can be done while still appearing much like traditional
text editing, which they call Language Boxes.[1][2]

Language Boxes only provide the means to compose syntaxes, but handling the
semantic composition of languages is left to the authors of the languages
which are being composed. Haskell is perhaps a good choice of language for
providing the kind of glue needed here, where we can decide where languages
can be composed based on the types returned by their parsers.

[1]:[https://www.youtube.com/watch?v=LMzrTb22Ot8](https://www.youtube.com/watch?v=LMzrTb22Ot8),
[2]:[http://lukasdiekmann.com/pubs/diekmann_tratt__parsing_compos...](http://lukasdiekmann.com/pubs/diekmann_tratt__parsing_composed_grammars_with_language_boxes.pdf)

~~~
chc
I don't think the problem stops at syntax. It's possibly an even bigger issue
that mixing different language semantics can be awkward. As a big obvious
example, a language where all objects are nullable will interface awkwardly
with one that only has option types. Similarly, interfacing with something
like Smalltalk (which uses methods for flow control) or Forth (which…is Forth)
would be awkward from a language that's more like C++.

Even in an environment like the JVM which specifies a lot of stuff for you,
it's awkward to call into Clojure from Java because of the semantic
differences.

~~~
sparkie
I wasn't implying there is no problem with the semantics, just that it's much
easier to deal with when you already have the parsed trees, because they're
easier to reason about with code - and we can project them unambiguously.

We already do write tools for such language interoperability for specific
pairs of languages, which is often really awkward because it requires us to
re-implement the parsers, and only deals with entire code files rather than
specific productions in the syntax.

It's pointless composing languages unless it makes sense semantically, which
would need to be done on a per-language basis (or per-production rule), which
is where I was hinting with using Haskell as the glue for such
interoperability - because if we encode the semantics into the type system,
such that one syntax expects a language box of type T in it's grammar, then
one should be able to use any other language whose parser returns a T, and the
semantics will be well-defined for it.

It could also provide the glue for converting between nullable types and
option types for example too, by requiring that a language returning a
"Nullable T" be wrapped in some function "ToOption", which converts "Nullable
T" into "Option T". Attempting to use the Nullable where an Option is expected
would fail to parse. How ToOption is implemented is left to the author of the
code.

It's much easier to have interoperability between individual production rules
in different languages (which share many parts in common) versus "whole text
files" which we currently have, which basically require the languages be
almost equivalent to convert between them.

Also as a result of storing the semantic information as opposed to sequential
text, it would be possible for the user to chose his preferred syntax for any
semantic elements in the tree, since they're just working on a pretty-printed
version. Most of the concerns about "code style" disappear because they're
detatched from the actual meaning that is stored.

------
Strilanc
Is this... a Let's Play of learning a programming language? Because it's
surprisingly effective. I learned and I was entertained.

~~~
gamegoblin
I would definitely watch a video series in which an experienced and talented
programmer tried out various programming languages for an hour and gave first
impressions, comparisons, etc as they hacked something together during the
video.

~~~
beefsack
The idea of a twitch.tv stream sounds interesting, with viewers throwing small
problems at the coder to solve as they fumble around with a new language.

~~~
supergauntlet
In this vein I've been pondering doing a livestream of a LFS install for a
while, just for funsies. I wonder if anyone would actually watch it.

~~~
MrException
I'd watch it!

------
brudgers
As others have said, this does justice to the idea of actually learning a new
language...or perhaps because it is Racket a new family or ecosystem of
languages. Anyway, if you're still curious about pairs verus lists and why
anyone would use dotted pairs, I like to think about it as where Lisps show
that they are from the age when running close to the metal was a given.

And it all goes back to _car_ and _cdr_ and the fact that they are (or rather
were) embedded assembly language and there to give raw access to Lisp's linked
memory model (as opposed to the sequential memory model of Fortran). A dotted
pair has two efficiency advantages over a proper list and both stem from the
fact that the last cell of the last pair of a proper list contains 'nil (or
'null in Racket).

Storing two values in a proper list requires two cons cells - the first with
the first value and a pointer to the second cons cell and a second cons cell
containing the second value and a null pointer. In contrast, a dotted pair
holds two values in a single cons cell - halving the memory requirement.

The second advantage is that when there are only two values there's no need to
walk the list and test for 'null (or 'nil) on the _cdr_. This saves an
instruction step.

Philosophically, dotted pairs allow for _car_ and _cdr_ to be used
symmetrically. Calling _cdr_ on a dotted pair returns the second value
directly just as calling _car_ on any list returns the first value directly.
Lastly, one of the things that is awesome about Lisp is the way in which lists
can model data structures, and in the case of a dotted pairs their
efficiencies are available to all those structures which consist of or rely on
paired values.

Of course, this may be obvious and on a machine with 10+ GB of RAM not really
applicable, but I find it fun to think about anyway.

~~~
ANTSANTS
My question is, given the cons pair is not a meaningful primitive datatype to
modern CPUs (can't fit in a register unless you're using 32 bit atoms on a 64
bit CPU, the individual cells cannot be meaningfully manipulated while packed
into a single register even in that case...), is there a good reason to use an
unrestricted pair/2-tuple/2 element vector for the sexp representation? Would
it be "wrong" to remove dotted pair notation and make sexps a restricted type
whose cdr could only be another pair or the empty list? You'd have to give up
alists, but those would probably be better served by doing as Clojure does and
introducing a hash table syntax.

Other than alists, simplifying the implementation[1], and tradition, I can't
think of a good reason to keep improper lists and dotted pair notation around.
They aren't really that useful, confuse newcomers, and encourage the use of
inefficient data structures. I haven't written much Lisp, but when I see a
dotted pair (usually in the context of an alist or some hideous approximation
of a struct[2]), it comes across as a "code smell" to me, like "this person is
taking SICP too literally."

[1] You pay for extra reader syntax, but gain uniformity in your treatment of
the car and cdr cells. On the other hand, maybe you could optimize the cdr
cell a bit if it only needed to address pairs and not arbitrary atoms.

[2]

    
    
      ;; barfage
      (define (make-thing a b c) (cons a (cons b c))) ; barf
      (define (thing-field-a x) (car x))
      (define (thing-field-b x) (cadr x))
      (define (thing-field-c x) (cddr x)) ; more barf

~~~
brudgers
Going a little Abelson&Sussman, the utility of _cons_ persists because it is a
powerful abstraction. Fitting into registers was, as a scholastic might say,
an as accidental feature as the input line voltage of a toaster [110 @ 60Hz
makes things easier in this part of the world but isn't intrinsic to making
crisp English muffins].

 _cons_ is useful for dealing with linked memory structures, including
singlely linked lists and particularly singly linked lists where the stored
value in each list element is a pointer because it abstracts over all the
pointers. Writing code to process and manipulate and pass pointers is
idiomatic in C. Using pointers is idiomatic Lisp.

Your barfing code shows why _cdr_ and _car_ persist. They are also powerful
abstractions due to their rules of combination and probably because their
rules of combination aren't constrained by a bias toward English language.
There ain't no synonyms for "caddr" and "cdddr" in English or Python.

The idea that lists should be everywhere replaced by structs is assembly in
any language thinking (also known as C). Lists provide a standard interface.
Each struct definition creates a new data type.

 _It is better to have 100 functions operate on one data structure than 10
functions on 10 data structures._ Alan J. Perlis, Epigram 9.

~~~
chc
I think the question is more along the lines of "Should the core data type be
the cons cell rather than the list?" Clojure takes the approach the GP was
talking about. It has lists with heads and tails, but it does not have dotted
pairs. The tail of a list is always a list.

------
Zoxo
I like the approach you took to writing this post. This wasn't the typical
attempt to summarize an entire programming language in a page (which IMO is
done too often, too poorly), but rather an exploration. As you went through
each section, I could see how you were approaching various problems and
learned a lot about Racket in the process. I would love to see more of this
style of post.

~~~
artyomkazak
Of course I couldn't summarize Racket in a page – I don't know it! And there's
_so_ much to learn about it that I could probably write half a hundred posts
of this length and still consider myself a beginner.

------
JimmyM
This is how I usually implemented quicksort in Racket:

    
    
        (define (quicksort xs)
        (if (null? xs)
        xs
        (let* 
            ([hd (car xs)] 
             [tail (cdr xs)]
             [smaller (filter (lambda (x) (< x hd)) tail)]
             [bigger (filter (lambda (x) (>= x hd)) tail)])
          (append (quicksort smaller) (list hd) (quicksort bigger))))
    

It's great to see a hugely superior implementation, and I love that people are
writing about and using Racket like this because the more people do that the
more resources there will be for people like me to learn from.

------
nextos
I love Scheme. And I love Clojure, which is IMHO Scheme plus some great ideas
from Haskell.

I regret there's no Scheme or Clojure running on LLVM, which I think is a much
better platform than the JVM. Julia, that resembles Dylan (another Lisp), is
the perfect example.

~~~
saosebastiao
I too wish there was such a thing as a JIT compiled Clojure on LLVM. The JVM
is amazing but it only seems to be amazing for long running processes. LLVM,
IMO, is far more versatile in its amazingness.

~~~
pjmlp
There was an attempt at it, [https://github.com/halgari/clojure-
metal](https://github.com/halgari/clojure-metal)

------
mahmud
That was intellectually honest. Good read.

------
kenko
It seems as if several of the author's problems come, oddly for someone who's
used both strict (Pascal) and non-strict (Haskell) langauges, from being
confused about Racket's strictness. Why is time a special form? Because
otherwise (time (expensive)) would just get the result of (expensive). Why
doesn't (list 1 (2 3) 4) work? Because list isn't a special form, it's a
function. Why doesn't quote turn '(list 1 2 3) into '(1 2 3)? (Well, this one
isn't about evaluation order, admittedly.) Because if it did it ... wouldn't
be quote.

~~~
artyomkazak
Pascal is strict, but it still has stuff like “move(var1, var2)”, out
parameters, etc., so it's also call-by-reference.

I didn't know at the time that Racket was call-by-value.

Quote was confusing at first, yes.

------
derengel
A friend of a friend has considerably code in Scheme, Common Lisp and Clojure,
when you ask him which is the true Lisp, guess what will he answer? Haskell!

Haskell does really create a big impact on some developers.

Just an anecdote ;)

~~~
dllthomas
I think it can't be "the true Lisp" without cleaner macros. Of course, that
doesn't stop Haskell from being a great Haskell.

~~~
Tyr42
I do prefer Racket macros to Template Haskell, but you can do some of the same
kind of things in Template Haskell. Go ahead and play with it for it for a
bit.

Mostly, you need to have the data-structure it defines for the parse tree, and
it's harder to convert than syntax->datum. Also there's the Q monad, to
generate new safe names. But it works pretty well.

~~~
dllthomas
I'm not sure there's anything you _can 't_ do in TH, but it feels messier than
Lisp macros. I'm hoping typed TH will improve things (not in precisely the
same direction as lisp, of course).

------
comex
One note:

> Racket's default IDE is better than GHCi and probably on par with Emacs (you
> almost certainly can configure Emacs to be better than anything, but it's
> not trivial and people don't bother, while DrRacket provides autocompletion
> and documentation out of the box).

Last I used it (a few years ago), DrRacket was very laggy, so I would find it
very hard to use for a serious project. YMMV, maybe it's improved.

~~~
brudgers
I find DrRacket sufficiently fast and sufficiently frustrating.

It's very useful for interactive debugging but dependent on the mouse and many
Emacs key combinations simply cannot be mapped because of it's CUA
interface...and so far as I can tell there is no key-combination that switches
focus between the REPL pane and the Editor pane short of closing the other
pane.

The syntax analysis gets to be a bit much too.

On the other hand, that's just the price of an IDE over a text editor, and for
a batteries included IDE, it's pretty good even if I wish that the effort
would have been put into Emacs support while knowing that doing so would not
meet the needs of the PLT group's target audience of students.

~~~
brvs
I think you can switch panes with C-x C-o, same as the Emacs combo.

~~~
aaronem
That's C-x o; C-x C-o in Emacs runs delete-blank-lines, and I'm not sure what
it does in DrRacket.

------
SmileyKeith
I love the play by play of exactly what you're thinking as you're doing
something new. I find them quite informative.

------
charlieflowers
Web apps are a hack on a hack, yes. But it's kind of ironic he hates
Javascript so much when Javascript is a pretty cool programming language
heavily influenced by Scheme, which is awfully similar to Racket.

But hey, I like people who call it like they see it, and there certainly are
some drawbacks to Javascript too.

~~~
ktg
Javascript is a pretty cool programming language heavily influenced by Scheme.
-> I couldn't agree with you more.

There is a Lisp-to-JS compiler (ParenJS).
[https://bitbucket.org/ktg/parenjs](https://bitbucket.org/ktg/parenjs)

------
toolslive
he will be amazed when he discovers custodians. [http://docs.racket-
lang.org/reference/eval-model.html#%28par...](http://docs.racket-
lang.org/reference/eval-model.html#%28part._custodian-model%29)

~~~
klibertp
There are many cool features that Racket has out of the box, like very nice
module system, delimited continuations, objects and classes and of course
macros (both hygienic define-syntax-rule, syntax-case and unhygienic defmacro)
and more.

But if I had to show one feature of Racked to make someone amazed, it wouldn't
be any of those. It would be a simple program composed of a couple of files,
and every file would start with different #lang. Like #lang racket, #lang
lazy, #lang typed/racket, #lang datalog.

It's sufficiently mind-blowing that there are this many languages on top of
Racket, but the real "killer app" is how seamlessly they integrate with each
other.

The next thing I'd show would probably be Danny Yoo tutorial on how _you_ can
create even more languages like this:
[http://hashcollision.org/brainfudge/](http://hashcollision.org/brainfudge/)

~~~
nextos
It's almost like a practical Mozart/Oz with all Lisp awesomeness. CTM shows
how programming with many different paradigms can be extremely powerful.

------
gambogi
"Someone who knows Haskell learns some Racket"

"Tonight at 11: this rose gardener learns to plant tulips"

~~~
artyomkazak
Which is exactly why I was very surprised when it got posted to Reddit and
wasn't immediately downvoted.

------
wglb
Nicely written, and a fun exploration.

