Uptight
Uptight is an infectious (as in IO monad) library answers two pain points of Elixir programming:
- Insufficiently tight types when it comes to distinguishing texts from binaries, as well as detachment of Base-encodings from binaries.
- Lack of an iron-gauntlet approach to errors and failure in Erlang and Elixir.
To address (1), we present the following types:
Uptight.Text, which is roughly equivalent to Haskell'sText. If something is made using "offensive" constructor (meaining that it will crash if you try to give it something weird), it's guaranteed to only contain valid UTF-8 codepoints.Uptight.Binary, which is simply a wrapper for anything that should be treated as raw binary.Uptight.Base, which is also a cool one. It has several subtypes defined, ranging fromSixteentoSixtyFouras well asUrlsafe. When you run a rarely usednew!function ofUptight.Base, it tries to decode the Base-encoded binary representation given as an argument starting from the smallest alphabet (mk16), then medium (mk32), then restricted large (mkurl), and finally it tries the large one (mk64). That said, most often we usesafeandsafe!functions, which takes a wrapped raw binary of typeUptight.Binaryand returns something ofUrlsafetype. In the opposite direction, the most used function ismk_urlandmk_url!, which takes an _unwrapped urlencoded string and stores it inUptight.Basevalue.Uptight.Foldis related to binary processing, namely text processing at the moment (but probably it should become an Elixir adaptation of Control.Foldl, perhaps in another library even). The only cool thing it allows for currently is genericintersperseandintercalate, which uses monoidal glue to glue semigroup bits in some foldable data structure. Practically speaking, it means that we can intersperse texts (like%Uptight.Text{text: "/"}) between a list of other texts (like [%Uptight.Text{text: "."}, %Uptight.Text{text: "a.out"}]) because list is a foldable data structure and string concatenation forms semigroup over texts.Uptightis a module which allows to transform naked binary, binary in a list, binary in second element of a tuple or binary values in a map into either Text (if it only consists of UTF-8 codepoints) or Binary. You probably shouldn't use this function because it's actually pretty lose and can cause surprises. Always prefer an explicit constructor.
To address (2), we present the following types:
Uptight.Resultis something that is already used in defensive (non-bang) versions of functions you saw in the previous paragraph. It's basically Erlang's{:ok, Value}|{:error, Reason}tuple, but rewritten in Witchcraft and named after Rust:Uptight.Result.OkandUptight.Result.Err. But it's more than just that. If that was it, we could've simply used Witchcraft'sEither.Uptight.Resultis also good for enabling offensive programming!
Offensive programming in Elixir
We often want to propagate an error to third parties, be it frontend programmers, end users or colleagues. It means that along with whatever context information BEAM generates for error handling, which often (but not always) is readable by Erlang/Elixir developers, we would like to also provide human-readable description of what went wrong, ideally together with some values.
In absence of blessed way to do so, I have reinvented came up with my own. It's a bit clumsy thus far, but all you need for it are assertUptight.Result and Uptight.Trace. Here's the "pattern" for writing failable functions in such way that error getting propagated all the way the frontend is still handlable:
defp poc_submission_output_to_raw_score(xkv, _opts) do
Uptight.Result.new(fn ->
# Demonstration that we can match against Ok, extracting the values
%{"has `order` field set" => %Ok{ok: vertices}} = %{
"has `order` field set" => Uptight.Result.new(fn -> xkv["order"] end)
}
%{"has `distance` field set" => %Ok{ok: distance}} = %{
"has `distance` field set" => Uptight.Result.new(fn -> xkv["distance"] end)
}
# We can do all the matches we would normally be able to do in Elixir
%{"solution vertices exist in the data set" => %Ok{ok: [{x0, y0} | ctail]}} = %{
"solution vertices exist in the data set" =>
Uptight.Result.new(fn -> path_to_coordinates(vertices, poc_data()) end)
}
{ref_distance, xf, yf} =
Enum.reduce(ctail, {0.0, x0, y0}, fn {x1, y1}, {d, x0, y0} ->
{d + distance({x0, y0}, {x1, y1}), x1, y1}
end)
ref_distance = ref_distance + distance({xf, yf}, {x0, y0})
# Even exploit one of my favorite capabilities of Erlang/Elixir's pattern matching:
# ** equality matching **! Note that rounded_distance and rounded_distance must be equal!
%{"distance reported is correct" => {rounded_distance, rounded_distance}} = %{
"distance reported is correct" =>
{(1_000 * ref_distance) |> round(), (1000 * distance) |> round()}
}
# Note that at no point are we actually doing any validation. We just state our intent,
# tagging matches with human-readable strings and let it crash, deferring the responsibility
# of catching the failure to `new` function of Result and deferring the responsibility to
# crash again to the user of our function. Paradoxically, having a somewhat defensive wrapper
# allowed us to crash as loudly as we want while writing code. It's an amazing feeling.
rounded_distance
end)
endThere's an issue in Doma's megalith repository aiming to fix error reporting ergonomics. According to Elixir macro gurus, it's possible to turn this pattern to a macro, because there are non-hermetic macros in Elixir!