Wrapping up `Practical SML#'
Review of chapters 9-11 & overall thoughts
2024-02-24
For Context: All the previous posts in this series
- Chapter 1: Setting Up an SML# Environment
- Chapter 2: The Essense of ML Programming
- Chapter 3: List Processing
- Chapter 4: Defining and Using Datatypes
- Chapter 5: Modules and Partial Compilation
- Chapter 6: Techniques of Designing and Developing ML-style Systems
- Chapter 7: Interoperability with the C Language
- Chapter 8: Accessing External Data
Looking back
In July 2023, I’d just received my copy of “Practical Programming with SML#” (SML#で始める実践MLプログラミング), and decided to blog about each chapter as I read it, as a form of making SML# a bit more accessible to the English-speaking blogosphere.
I started out with a pretty good cadence, working through the first three chapters before the end of July. Then, my pace flagged, and chapters 4-8 took me the rest of the year to write up.
It’s now February 2024 and I’m wrapping up this series, without having reviewed the most meaty and intriguing final three chapters. The truth is that my enthusiasm towards SML# as a hobby has gone down considerably, and most of my (very scarce) recreational programming time is going towards plain SML these days.
The rest of this post will contain a brief synopsis of the last three chapters, and then my overall impressions of the SML# ecosystem, based on my admittedly cursory engagement.
Chapter 9: Cooperating with Databases
This chapter walks us through the process of defining and persisting our application data in a relational database. As the other chapters in the book, it’s very complete, and has us do everything from establishing a database connection, through to INSERT and UPDATE commands, handling conversion and query results, and finally running an analysis of the COVID data (imported using the JSON conversion features introduced in the previous chapter).
Having felt enough professional pain from tight coupling of application models and logic to the DB layer (ActiveRecord, SQLAlchemy, etc., and to a lesser extent Ecto), I was not exactly super enthused about this chapter and the approach taken by the language designers.
Essentially, as I understand it, SML# uses its built-in reflection and dynamic typing capabilities to build SQL statements based on SML#-level type information. This means that the entities on the SML# side and the entities on the SQL side must align 1:1 in terms of naming and semantics. Perhaps this approach is in line with the KISS philosophy, and forces application developers to keep their application-side code wholly reflective of the reality in the DB.
Maybe I’m spoiled from having been exposed to the Haskell way of solving these issues with Typeclasses, but to me both the JSON conversions and the SQL mappings are a kind of false economy, making the easy things easier but the hard things impossible. I’d prefer to do a bit more work up-front defining converters or parsers (as the Elm language forces you to), than to acquiesce to 1-to-1 mappings, and therefore dependencies, between my application code and serialized data.
(To be pedantic: it’s possible, of course, to have a layer of SML# code that serves as the ‘parser’ layer, and then create truly pure models from this ‘parser’ layer, but practically speaking no one is going to go to these lengths in the name of elegance or flexibility.)
This is the chapter that probably “lost me” to the cause. But, being so close to completing the book, I decided to skip implementing the code in this chapter and jump straight to parallel programming.
Chapter 10: Parallel Programming
Having used Haskell for several years, and then Erlang and Elixir for most of
my programming career, I came to this chapter with high expectations.
Unfortunately, I wasn’t really able to get my hands dirty with the material,
because Pthread.Thread.join
would keep hanging my smlsharp session, and only
a SIGKILL could get it unstuck. Yes, the thread I was trying to join had
finished doing the work. Yes it did print the result after getting SIGKILLed!
(You might be thinking that I should have gone and debugged this strange and
interesting concurrency bug. That’s true, but I really felt at this point that
the juice was not worth the squeeze. Keep reading for more on this topic).
I really wanted to see the pretty ray-traced pictures, so I implemented the single-threaded raytracer in regular SML, and compiled with polyml. It compiled and ran like a charm!
At this point I was already disheartened enough that I didn’t proceed with the MassiveThreads parallel raytracer discussed in the rest of the chapter.
But to summarize the remaining part: in contrast with OS-level threads that regular SML implementations use, SML# offers drop-in support for a “green-thread” system called MassiveThreads. These are much cheaper to initialize and run, enabling finer-grained parallelism and better utilization of CPU resources.
Chapter 12: Techniques of Developing Practical Systems
This chapter is a synthesis of all the previous ones: we have a fully-fledged C-integration with Cairo (producing PDFs no less), a database in sqlite, and command-line parsing. The chapter sets out to prove that we can realistically apply SML# to the real-world task of plotting datapoints from a relational database.
Epilogue
The epilogue drives home the key points made by the authors in the course of the book. There are four fundamentals of SML:
1) Think in types
2) Write functions while keeping recursive structures in mind
3) Express the problem with data definitions
4) Use make
and incremental compilation
And four special characteristics offered by SML#:
5) Access software libraries via C
6) Ingest external data safely with dynamic typing
7) Directly program the database
8) Use parallel computation features, harnessing multicore CPUs
My thoughts on the book, the language, and the ecosystem
First of all, a disclaimer: I read the book solely for personal enjoyment, in a hobbyist capacity. Perhaps working through it in either an academic or a professional setting, with experts on hand to talk to, would have been a different experience. I also didn’t implement any larger program apart from the examples and exercises from the book.
With that perspective clear, here are my thoughts.
1. This was one of the best programming books I have read
I would place it right alongside “Common Lisp: A Gentle Introduction to Symbolic Computation”, “Structure and Interpretation of Computer Programs”, and “Erlang Programming”, my three personal favorites.
It’s not just a well-written book, but it’s a book that accomplishes what it sets out to do: impart the authors’ knowledge on the reader. There’s never any doubt as to what the authors are trying to convey, and every bit of code has an explanation. My Japanese reading skills are nowhere near native level, yet I never felt lost or confused by grammar or vocabulary choices.
There is a lot of code in the book, interspersed with discussion, so the pace is fast and always feels engaging. The SQL chapter is a bit of an odd one out, sometimes coming stylistically closer to a reference manual than to a tutorial.
Then there are the exercises, which directly reinforce and often expand on the material from the preceding chapter. In my mind, this is the gold standard for a practical programming book. I really enjoyed doing these exercises, and thanks to this I retained much more of the presented information.
2. The language is an unqualified ergonomic improvement over Standard ML, with some raw areas
SML# the language is the reason I bought the book, and I wasn’t let down. It improves on the standard in many subtle but very pleasant ways, from thorough Unicode support to the ability to selectively match on records, really bringing the developer experience into the 21st century when it comes to programming in the small.
The Dynamic, SQL and Foreign extensions to the language feel a bit raw, like
the authors are sharing with us the internal implementation of features not yet
fully completed. These modules are very powerful but somewhat arbitrary, like
Go’s special treatment of map
s and slices, or StandardML’s own special
“equality types” and math operator overloading. It feels like some parts of the
system have been given superpowers, but the user has only the ability to plug
in to these superpowers, and not to extend them.
I can imagine these features seeing more involved development. On one hand:
allowing user-level extensions (such as custom Dynamic.fromXYZ
converters),
and on the other, building higher-level features (such as deriving
-style code
generators) that use these extensions under the hood. We already know it’s
possible, given how smoothly the JSON and SQL intergrations work.
3. The tooling is underwhelming given its ambitions
There is a lot to like in the smlsharp
tooling. The system compiles
cleanly (*once you sort out massivethreads), includes high-quality SML libraries
(smlnj-lib
, smlunit
) and tools (smlyacc
, smllex
, smlformat
), and the
make
integration is a very welcome change from endless language-specific
build tools. And yet, there are a couple things that prevent the experience
from really taking off.
First: the compiler is just slow. The authors at several points make the case that the main goal for SML# has always been extending the language in a practical direction for usability, and this came at the cost of performance work. I appreciate this approach and think it’s the correct one. This approach gave us Lisp and Erlang and Haskell, and each of these languages is —nowadays— sufficiently performant for real-world usage.
Still, with that understanding, the compilation process is almost unacceptably slow for day-to-day use. Here are the compilation times for the ray-tracer program:
% time mlton main.sml
Warning: main.sml 98.14-98.21.
Declaration is not exhaustive.
missing pattern: NONE
in: val SOME y = Int.fromString (hd args)
real 0m4.188s
user 0m2.459s
sys 0m1.482s
% time smlsharp main.sml
main.sml:98.14(2768)-98.46(2800) Warning: binding not exhaustive
SOME y => ...
real 0m1.069s
user 0m0.868s
sys 0m0.195s
% time polyc main.sml
main.sml:98: warning: Pattern is not exhaustive. Found near val (SOME y) = Int.fromString (hd args)
real 0m0.156s
user 0m0.112s
sys 0m0.045s
So… not as slow as mlton
, but still several times slower than polyc
. This
really adds up, and works against the ‘practical’ bent of the language. Fast
compilation, and therefore developer feedback, is why Turbo Pascal was so
popular back in the day :)
Second: The make
-based incremental build system has some warts, and the
.smi
interface-file scheme feels like a throwback to the early ’90s. I don’t
enjoy having to write practically the same code in two places, and that’s
effectively what you end up doing. Again, maybe I’m spoiled by Haskell’s module
system with explicit imports and exports, but the .smi
-file dance feels to me
like something the compiler/build-system should be doing for me.
Also, regarding the clunkiness of the make
system, here’s a line from the
last chapter of the book: the chapter which, mind you, demonstrates the full
extent of real-world programming with SML#:
$ smlsharp -MMm dbplot.smi > Makefile
$ sed -i.orig -e '/^LIBS/s/$/ -lcairo/' Makefile
$ make
First, we make some edits to our interface file, dbplot.smi
. As I noted
above, this seems like an unnecessary step, at least for most usage. (I know
there is a special provision for mutliple files exporting the same interface).
Anyway, okay, we edited the .smi
file, so we have to regenerate our
Makefile
. Okay, let’s say we agree to this step. But the next step is
unacceptable: we have to go and re-write our autogenerated Makefile
with
sed
.
This is a violation of DRY: I don’t want to have to re-run sed
on my
makefiles everytime I change an interface file. I want to specify somewhere
that my code depends on -lcairo
and not have to continuously re-jigger the
build artifacts by hand. I even made a Pull Request with a proposal for a
fix… which brings us to the
worst aspect of smlsharp, and the real dealbraker when it comes to real-world
adoption. The lack of publicly active users.
4. The community is inactive, to put it politely
For a project of such high quality and high visiblity, SML# feels like a ghost town. Since the book came out in 2021, there have been a meager 23 commits to the github master branch, the last one in March 2023. There isn’t any discussion on the PRs that folks (including myself) have put up, and the github forums in both Japanese and English have had zero activity since early 2022.
To compile massivethreads
on a reasonably modern
GLIBC, you need to find the debian
patch
in a developer’s personal fork. This hasn’t been addressed in the smlsharp master
branch README.
There was some activity on Japanese programming Twitter around the time the book came out, but sadly no one really ran with it for a longer period of time.
In general, from my 7-month long engagement with smlsharp
, I got a taste of
that famous Japanese feeling of
wabi: the presense of something
brilliant, deep, meaningful, and yet always distant, empty, and already
fading away.
So there you have it. For my recreational programming, I’m going to be sticking
with the “standard” Standard ML and the blazing fast poly
compiler. If you’re
up for some SuccessorML-style experimentation, you’d do much better to look at
LunarML,
which is very active and very, very promising. And also Made in Japan!