DEV Community

Zelenya
Zelenya

Posted on

What makes library docs great [Library review: Iron]

šŸ“¹Ā 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."

Readme

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
Enter fullscreen mode Exit fullscreen mode

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

Overview

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"
Enter fullscreen mode Exit fullscreen mode

šŸ‘€ 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.

API docs

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])
Enter fullscreen mode Exit fullscreen mode

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)  // ???
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

šŸ’”Ā 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",
Enter fullscreen mode Exit fullscreen mode

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 _)
Enter fullscreen mode Exit fullscreen mode

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
  • 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:

Top comments (0)