📹 Hate reading articles? Check out the complementary video, which covers the same content: https://youtu.be/4pq1elOap9k
Using most of the libraries for the first time usually sucks. But it doesn’t have to be this way.
Iron is a specific library for a specific use case, but we can all learn from it. It’s a simple no-bullshit library with great docs. Let’s review it and talk about documentation from the onboarding and user experience perspective.
Initial impression
Here is how it usually goes. I see an unfamiliar library in the imports or dependencies, or a colleague suggests one. I look it up and go to the GitHub page.
👀 Note: I’m looking at the library (and readme) at this point in time.
First plus – right away when opening the readme – a concise description: "what the library does, why I should care, and a bit of how."
It doesn’t assume that I know what it does, nor what refined types do.
It’s impressive how often libraries don’t do this! And I spend significant time jumping through the docs and forums just to figure out what the library is for.
Readme also links to the microsite; we’ll return to it shortly. Let’s scroll through the rest.
The next part is a little example:
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.*
def log(x: Double :| Positive): Double =
Math.log(x) // Used like a normal `Double`
log(1.0) // Automatically verified at compile time.
log(-1.0) // Compile-time error: Should be strictly positive
val runtimeValue: Double = ???
log(runtimeValue.refine) // Explicitly refine your external values at runtime.
runtimeValue.refineEither.map(log) // Use monadic style for functional validation
runtimeValue.refineEither[Positive].map(log) // More explicitly
It showcases imports and elementary usage, so we know what to expect. The snippet is small enough to be quickly digestible but still representative – we see how to refine a type as positive and right away how to use it at compile and runtime.
Then, it briefly demonstrates error messages, dependency for sbt and mill, platform support, adopters, and useful links.
If this isn’t perfect to-the-point readme, I don’t know what is.
Great expectations
Quick side note: here’s what I typically want from the library site or the docs.
When it’s my first time using a library:
-
Getting Started Guide
- I want introductory information every developer will need.
- Such as an overview of the library and its components, a Hello World tutorial, and an introduction to the fundamental concepts.
- (And I don’t want to read a whole book full of definitions right away.)
- Bonus points: If the quickstart docs are usable for returning users, for example, when setting up a new project.
-
Tutorials and concrete topics
- (“That book” that I didn’t want to read right away.)
- I want to dive deeper into the library as I’m getting familiar with it and wish to extend my usage or knowledge.
- I don’t mind if the documentation holds my hand while we’re walking through the steps at this point.
When I’m working with a library:
-
How-to guides and examples
- I want task-based instructions for how to do something or solve common problems.
- I expect conceptual content organized by topic or task.
-
API Reference:
- I want readable API docs – the actual public API, not the internals of the sausage.
- It should show how to create “things”, “interact” with things, and so on.
Microsite
With this in mind, let’s see what Iron’s microsite offers.
☀️ It offers a day/night toggle; who doesn’t like a good day/night toggle.
The welcome page exhibits links to navigate the docs and some code examples.
Discover
The Overview page introduces the fundamental concepts: the purpose of this library, why refined types matter, and the use cases. It also includes a tiny hello world snippet, which we’ll try soon. This page is a big part of the getting started guide I wished for.
And then, it links to the Getting Started page to set up and start using Iron and References for details about the concepts of Iron.
Getting Started provides the rest of the getting started guide: dependency and standard imports. The import sections cover what they bring – which implicits and functions.
libraryDependencies += "io.github.iltotore" %% "iron" % "2.1.0"
👀 Notice that the header (in the top-right corner) shows the library version and allows us to navigate the docs at that point.
💡 Other pages on this level are Code of Conduct and Contributing. But we don’t care at this moment. We’re exploring the library and not planning to contribute anything right now.
Reference, not reference
The next section of the docs is Iron references, where we can “find detailed documentation about the main concepts of Iron”.
Note that this is not the API Reference I introduced before – from my perspective, this section includes tutorials and how-to guides.
Tutorials: Iron Type, Refinement Methods, Constraint, and Implication. These cover the main datatypes and how to use them. After going through these sections, I could start using the library – I felt confident enough and didn’t feel like a learner anymore. These docs have concrete, practical examples.
How-to guides: Creating New Types. It shows how to create no-overhead new types using opaque types. Which is excellent, just a different type of documentation – it doesn’t fit the rest. This is not something a first-time user needs right off the bat.
Modules
The last section documents how-to connect external modules: how to add support for JSON decoders, how to support validation, etc.
… “support”/“interoperability” modules that provide out-of-the-box features to make Iron work seamlessly with other ecosystems.
Each page includes a short description, dependency, imports, and a how-to guide or an example.
Scaladoc
We can seamlessly switch to the API docs – the API Reference we work with while using the library.
The definitions are pretty concise but easy enough to navigate. Somehow it’s more pleasant than a typical Scaladoc.
🤔 I usually avoid Scaladocs. I don’t understand how to navigate them and go to the sources.
Putting it to work
While we’re here, let’s try using this Not
constraint. We can modify the hello-world example:
case class User(age: Int :| Not[Positive])
Compared to using just Positive
, this refinement type has an opposite effect:
-
User(1)
doesn’t compile (Could not satisfy a constraint
); -
User(-1)
compiles.
🤔 What do you think happens if we add another Not
?
case class User(age: Int :| Not[Not[Positive]])
User("1") // ???
User(-1) // ???
Exercise for the reader.
Adding json support
And to make it more interesting, let’s add a json support using circe.
To get encoders and decoders for refined types, we have to add an iron-circe
module:
"io.github.iltotore" %% "iron-circe" % "2.1.0"
💡 It doesn’t say which circe dependencies the example relies on, which might be a hurdle for people who never used either of the libraries. We can add dependencies from circe Quick Start:
"io.circe" %% "circe-core" % "0.14.1",
"io.circe" %% "circe-parser" % "0.14.1",
"io.circe" %% "circe-generic" % "0.14.1",
And then, we draw the rest of the owl using the example provided on the page:
import io.circe.*
import io.circe.parser.*
import io.circe.generic.auto.*
import io.circe.syntax.*
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.*
import io.github.iltotore.iron.circe.given
case class User(age: Int :| Not[Positive])
User(-8).asJson // { "age" : -8 }
decode[User]("""{"age": -8}""") // Right(User(-8))
decode[User]("""{"age": 18}""") // Left(DecodingFailure _)
Expected shouldEqual
Actual
When it’s my first time using a library:
-
Getting Started Guide
- Overview covers an introduction to the fundamental concepts and a tiny hello world.
- Getting Started covers the dependency and common imports (also handy for returning users).
-
Tutorials and concrete topics
- Some pages from Iron References cover main datatypes and how to use them.
When I’m working with a library:
-
How-to guides and examples
- How To Create New Types from Iron References shows how to do a concrete thing.
- Modules shows how to support external modules (interoperability).
-
API Reference:
- API docs (aka Scaladocs) show how to create “things”, “interact” with things, and so on.
In Summary
The funny thing is that I wasn’t even a fan of using refinement-type libraries. For some reason, I used to believe they were ugly and there are other ways to check if the string is empty.
But then, the other day, a colleague was migrating some code to scala 3 and found that the existing library has no scala 3 support. I heard of Iron, so I suggested taking a look. And we were both pleasantly surprised.
The library and its docs looked so nit; I just wanted to start using it and talking about it.
Cause it’s a “review”, the grade is 4 docs out of 4.
💡 Useful links: