This year I choose to do Advent of Code using Roc Lang. I wanted to talk about my experiences as it was pretty fun and I learned a lot.
Background
Advent of Code is like an advent calendar, basically a way of counting off the days until Christmas / Santa arrives by having a surprise each day. Advent of Code does that by giving you 2 small code challenges each day that get progressively harder. You can accomplish it however you want, with whatever technologies/programming languages you want. As far as I know, all of the challenges are pure functions. For the type of work I do, web application development where you’re mashing libraries & data together with only a little actual domain logic, I don’t ever use any of the algorithms they often want you to utilize. So for me, they’re extremely challenging, and a great way to learn a new language, or learn more about an existing language I already know, + all the algorithms I never get to use.
This year, I chose Roc. It’s a strictly typed functional language. As someone who loves Elm, a strictly typed functional language for front-end web development, I was lacking a language for everything else (server, cli, etc). ReScript has been my stand in for various reasons. I was excited to try Roc lang for last year’s Advent of Code, but my youngest daughter convinced me to use Lua for Roblox. I figured after a whole year, things would be much more stable now.
Why Strict Functional
My journey into Functional Programming taught me about a lot of things to love. In particular:
- managed effects
- no runtime exceptions
- immutability
- sound types
- no null
These have a ton of benefits, but the 2 I like most are easier unit testing, and more predictable code that’s easier to change later on w/o a lot of unit tests.
Having that for web development is great, but web development has a swath of things around it, namely Node.js and tooling used to build the web app through CLI’s and libraries, as well as various API’s to get and process data. Elm doesn’t build those. Which means, you have to choose from the various functional language options which I have opinions on.
Why Roc
Roc was inspired by Elm so has the 2 things I’m most excited about: managed effects and soundly typed an ML style that is like Elm. Meaning, you can write code without types, very similar to how you do in OCAML / ReScript, and it’s smart enough to infer what you meant. If you run into issues, or need better documentation, you can add type information later. This gives you the ability to play when you’re unsure of how things should be, or use hardcore types when you know exactly how things should be.
Roc also fills the gap for non-web frontend things. I can build servers, back-end API’s, and CLI tools. Hawt.
The Good
The things I liked most about Roc are:
- compiler errors
- speed
- Elm/ML like syntax
- Pipes with Property Plucking
- thorough modules
- tests in file
- tags are magically defined
The compiler errors, even at this early stage, are similiar to Elm’s; aka super helpful. They tell you what’s wrong, what line, what column, and suggestions on how to fix or why the error occurred. The compiler teaches you the language, and/or teaches you why your types aren’t lining up quite right.
Second was the compiler is mostly fast for a project where they aren’t done. I was only doing about 60 lines of code, but even TypeScript can take seconds. For something compiling to native code, this is dope. I’m not expecting OCAML instantaneous stuff here, just anything faster than TypeScript.
The syntax is super similiar to Elm’s or any ML language. If you know Elm or Haskell or F#, you’ll feel right at home. You can write a bunch of functions in a file, and it just works. If you want to learn about the types, or just document your stuff better, you can optionally add type definitions atop of your functions. Anonymous functions all over the place as you learn things requires just a backslash (\) and an arrow (->). Lots of whitespace and tabbing, super dope.
In Object Oriented Programming, you’re supposed to “compose” things into other classes; that’s how you build an OOP program. In Functional Programming, you compose functions together. The most common way is to use pipes to wire functions together, and you wrap that in a function. The pipe (|>) in Roc works the same as Elm, F#, and ReScript. It feels data first which for Elm/F#/Haskell people may take some getting used too, but after spending a few years in ReScript, I was ok. Also super convienant was the ability to pluck a field off of a record in pipe’s simply by going dot property (.property) at the end of a pipe.
Despite early days, the List, Str, and Dict had what I was expecting to work with those data types. That made most of the AoC code puzzles all on me vs. having to build my own helper functions for basic data types.
I liked how the expect keyword gives you basic unit tests in the same file you’re working in vs. having to create a separate module. Elm and F# taught me a lot about how you don’t need 50 billion files like you do in OOP code bases; you can just put a bunch of functions and types into a single file and you’ll be fine. I didn’t think about also adding unit tests, but… it makes sense. Unclear how this would affect my views of using modules to test their internals, meaning, you only write unit tests on the functions that are actually exposed, but… again, the idea of using Roc to play like you would in a dynamic language has merit here, and writing unit tests in a TDD style to figure out things is legit fun and helpful.
Lastly, I liked how you could just make up tagged unions on the fly in your function, and b00m, it worked. In Elm, you need to define these first as type Thing = A | B. In Roc, you just put A and B in your function, and Roc is like “Cool, looks like everyone is using A or B correctly, we’re good here”.
“What… no signature required? You don’t need to see my ID?”
“lol, naw bro, I’m Roc, I’m chill, maaaahhhhhn”.
“Dude… thanks!”
This last one seems trite because it takes 2 milliseconds to define a tagged union in Elm, but not breaking your creative flow, being inside the function and having the ability to just make up an type tag to return really helps when you're exploring.
The Challenging
Below are some of the challenges I ran into. Some of these might actually be me just needing to learn more. They are:
- compiler bugs
- panics
- Dict key Not Nat
- 20 billion numbers
- roc dev is strict right now
- roc test doesn’t show failure
- I’m too dumb to use dbg
On 2 of the puzzles, I ran into compiler bugs. When you create syntax a certain way, it’s clear their parser in Rust or whatever gets confused about what I wrote. To work around it, you just comment out the bad portions (yay for pipe workflows), or change how you write the anonymous functions, perhaps putting into their own function and sometimes with type definitions to help the compiler out. Other times, I never figured it out, and just tried a completely different implementation or data type. As I’m learning, unclear if this was my fault, the compiler, or both? No worries, I knew what I was signing up for. I’ve seen the Roc team chewing through these issues then having new nightly releases so it seems they’ll be crushed in future.
Roc doesn’t have exceptions. This means, like Elm or Go, all potential errors are returned from the function as a value, in this case a Result type. Everything from basic things like getting an item in a list, to converting strings to number types. … except… panic’s are basically when your program crashes. In fact, Roc has a crash keyword. This was really hard to understand. As a front-end developer, you never want to crash the UI; there is a user using it, and a broken UI doesn’t tell the user how to fix it; typically a modal or alert back saying refresh is better than a crash. Back-end people are put into horrible situations, such as no memory left, and no page file to write data too; they literally have no other option but to say “dude, I cannot do what you want me to because of this reason” with the faith that was logged somewhere. I get it. I'm still confused how to use it given the best practice is return a Result error from things that fail. This is why Go really put me off because they have an amazing way of handling errors like functional languages do, but then still offer the developer the ability to crash the program from any place via calling panic. I know there is someone about to lecture me on no way to return a Result Err from a function that fails because there is no memory left, and that includes in the electrons in the RAM to allocate the Result Err. For now I just avoided crash on principle, but did make sure to read how others were using crash in their programs.
Dictionaries, currently, cannot use Natural numbers as keys. As someone who has no clue why you need 20 billion number types, I just default to Nat, and that particular number type doesn’t quite yet work in Dictionary keys. It will soon, tho. I just switched to strings.
Yeah, about the 20 billion number types. I remember when I first learned some Go, and I was like “How do I cast this value to a Number” and the docs were all “What type of Number?” and I was like… dude, there is only 1 number in math, what do you mean?” and they were all like “blah blah blah signed blah blah unsigned” and I was like… “why does this matter? I’m just returning JSON, dear god…”. Smart people tell me there are performance, OS, and math reasons and these are powerful and great and I should be happy with all these choices. I’m too dumb to know what number to use. All I learned from 5 days with Roc is U8 is too small for normal things, Nat is super dope but breaks in Dictionaries, so use U32 4 good life.
Roc dev is dope in that it does both type checking for the compiler, compiles your app, THEN runs it. The issue, though, is if it spots dead code, it stops… although you can still use roc run
to run it. In Elm, the opposite happens; you’ll have a function, have no clue why it’s not working at runtime, and realize you never called it, hence Elm never compiled it into your program. When you’re just exploring ideas, having to comment various sections out and uncomment again over and over was painful. However, soon they’ll remove that unless you want strict compiler on which is awesome.
I like roc test
, but I don’t like how it’s like “yo, your test failed!” Coming from Mocha in JavaScript and PyTest in Python, they’re pretty verbose in telling you why it failed, specifically what your expectation was expecting vs. what it got. I know it’s early days, but dbg
(Roc’s console.log
) wasn’t working for me in roc test, so I was blind.
I’m unclear how dbg works. It’s not like Debug.log in Elm or console.log in JavaScript. It works if it’s in 1 place, out of a loop, and doesn’t have a lot of data to show. Logging is how I debug functional programs (I haven’t used breakpoints since my OOP days), so not having that consistently work like I thought made learning certain failed runs of my code hard. “It compiled but didn’t work, hrm…”. I need to read more on the Zulip chat, I saw a lot of advice on how to use it properly.
Adjusted Expectations Compared to Elm & ReScript
Roc had me adjust my expectations coming from Elm and ReScript. These are:
- function definition syntax
- no partial application
- data first
- no basic string manipulation
In Elm, a function definition requires no commas or parenthesis, and calling a function is the same. In Roc, calling a function requires no commas or parenthesis, but defining it requires a \ and commas. I can see how this makes reading the code easier to spot a definition vs. a function call, + more flexible to simply use anonymous and regular functions easily. That said, after 6 years of wiring my brain to write + call functions this way in Elm and F#, it was hard to adjust at first.
In Elm, I do try to avoid partial applications as I feel it just makes the code confusing for future me, or beginner developers. There are tons of cases, though, where it’s obvious, like using basic List functions in pipelines. However, I don’t think Roc supports creating partial applications; if I did the compiler would say it doesn’t support it. I know you can in pipes because all the List methods you could omit the 1st parameter and it’d “just work”, but if I started nesting partials or building my own, the compiler was like “no”. On the one hand, I like this as I think it helps make the code more clear; there is a reason point free coding is jokingly referred to as pointless programming. On the flip side, though, it is useful in many common situations when composing functions, so I’ll have to learn more why it only worked for “1 level” in pipes.
Data first is still hard for me, but may get easier in Roc since I won’t be designing for partial applications like in Elm? Even my 2+ years in ReScript hasn’t completely given me the ability to flip between data first and data last easily. I think Roc is smart making it data first, but still giving you the ability to pipe things together like normal, without having to define holes when you’re confused like I often get in ReScript (this entire paragraph could be wrong, I spent 2 minutes trying to figure this out).
Not having Str.substring, and Str.charAt was… really hard. The whole concept of converting it to scalars or utf8 and natively working with “strings as numbers” was whack as I’ve never done this. I’m used to playing with basic characters in JavaScript and Python, and super common thing in UI dev, so it was odd having to learn this new way of working with strings. I should of built my own methods, I just ran out of time.
Conclusions
Overall, I immensely enjoyed my time learning Roc this year. Roc clearly is poised to fill that gap that I’m using ReScript for currently. For AWS, ReScript was great because it compiled to Node.js, so I could deploy super easy. For Roc, it’s compiling to native code, which means I’ll have to switch back to Docker, or perhaps some type of WASM deployment. It’s too early in the game to tell if that’ll be a problem or not. As someone who’s a serverless fan and really hates dealing with servers and Docker, I’m just keeping the faith that’ll not be a problem in a couple years. The Fullstack push for many front-end devs means we have to do more Ops work, and I only slightly enjoy that work if it’s serverless. Dealing with Kubernetes, AWS ECS, and anything Docker related is miserable. I’ve seen thigs evolve here, so my hope is if I ignore it, it’ll either get easier in a couple years, serverless will make it easier, or someone else will do the server Ops.
It’s also clear from looking at the other Roc entries for Advent of Code on the Zulip chat that the varied backgrounds still result in different code, yet I can still read it. While I might of been pretty slack with error handling (e.g. tons of Result.withDefault
all over my code), and others were pretty hardcore with pattern matching with crash to ensure it failed fast with good errors… we all followed the same premise of types, records, and with some type of function composition. This, compared to OOP with inheritance vs. composition, various artistic interpretations of design & architectural patterns, and many who are just imperative fans means it’s always a crap shoot if you can grok the code. I know Roc’s surface area is small right now, and the AoC puzzle solutions were small, but even so, developers are creative, and… even in that expressed creativity, I was like “Yup, I can read this, understand it, and I don’t even know this dude”.
As someone who fell in love with Elm giving me the pinnacle of a front-end developer with 100% assurance all errors are provably a back-end developer’s fault, I was hoping for the same in Roc, and freaked out when I learned Go-like panics were a thing, and the crash keyword was native. However, from talking to “systems people” (you know, the people who code C++, Go, Rust, Zig, etc), they say stuff like “great power comes great responsibility” specifically around certain bad situations, which I get. I’m still emotionally dealing with this, but the short is I’d prefer this over what constantly happens to me in ReScript where I have a type definition to an external JavaScript function and it fails at runtime “because JavaScript”. It’s still a massive step in the right direction, especially from a testing perspective.
The promises of Roc are true, and future is super bright with Roc in it. Really great way to end the year.