š¹Ā 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: