-
How to Think About And Use the Option And Result Types
This post is written with an aim to provide some clarity as to the purpose and usage of the (evermore present) types
Option
andResult
in various programming languages.
The Beginning: the
bool
typeWe’re all used to using the
bool
type: its value set has cardinality2
, basically admitting two values:true
, andfalse
.
This means that we can use it as a return type for a function to discriminate between two outcomes from the function’s execution. Thus, it ties well with predicates (functions answering a decision problem, e.g.
isDiscountValid
orany?
) and, more broadly, functions that want to indicate a successful outcome or a failure outcome after their execution has finished.The return of a
bool
-typed value allows these functions to have great synergy with conditional statements/expressions, directly plugging into the antecedent (condition) part of a conditional statement. Undoubtedly, you’ve come across something like this (in C-like pseudocode):bool do_something() { // --snip-- if (something_went_wrong) { // signal failure return false; } // all went right in the main path return true; } int main() { // --snip--- if (do_something()) { printf("Operation was successful!\n"); } else { // perform some logging, or error recover here... } }
This… works. But it’s a fairly rudimentary mechanism, with a significant limitation: what happens if we want to pass along some information in either case?
This is a thought that has crossed many a programmer’s mind, and different languages have provided different solutions to this problem, depending on whether the language allows for one or more return values. Solutions fall within the following general criteria:
- Return a value if successful and
null
/nil
/None
otherwise. - Nest the
bool
and thevalue
in astruct
/class
and return that. - Return a single
bool
value and extra values in out-parameters. - Allow the return of
tuple
s containing the values. Option
/Result
/Either
.
Let’s have a quick look at some examples. For each of the below examples, imagine that our use case is the following:
We have a function which searches a rich document (think like a Word document) for a particular search text.
This is also called a “needle-in-haystack search”.
Return a value if successful and
null
otherwiseThis was pretty much the default way people handled this situation in Algol-family languages (C, C++, Java, etc.), especially in the past. It would look something like the following (in Java-like pseudocode):
public class Document { // --snip-- class SearchResult { int column; int line; } public SearchResult searchText(String needle) { // --snip -- if (found) { return SearchResult(column, found); } else { return null; } } }
The approach was fairly similar in C and C++, where instead of
null
someone might seereturn NULL;
or the more modernreturn nullptr;
in the case of C++.This approach came with a major, major flaw, one that has even been given its own nickname across the industry, by its own creator (Sir Tony Hoare) nonetheless: the billion-dollar mistake.
The problem is that the return value is usually then perused in a way that gets dereferenced (either directly, through a pointer, like in C and C++ or indirectly through objects in Java/C#, etc), causing the program to crash with either a friendly error message or
an occult incantation out of Saruman's grimoirea less descriptive error message, depending on how well-behaved the language runtime you’re using is:// In Java Exception in thread "main" java.lang.NullPointerException at Printer.printString(Printer.java:13) at Printer.print(Printer.java:9) at Printer.main(Printer.java:19) // In C/C++ [1] 93157 segmentation fault ./a.out
The computer really doesn’t like it when you force it to go to address
0x00
, you see. But…Nest the value and the
bool
in aclass
and return thatSeeing as we’re very sensitive to the computer’s plight (and our own inconvenience and embarrassment), we might as well try to find a different way: one that, at least, doesn’t result in a crash.
One way to solve the above problem, especially in the past, would be to wrap both the
bool
and the return value (in the successful case) in a single compound type (aclass
), and return values of that type. It would look something like the following.Assume that our string-search function has been programmed to return a
SearchResult
type of the following form:struct SearchResult { bool success; int column; // present only if text was found int line; // ditto };
Objects of that type would use the
success
sub-component as a discriminating value, indicating on whether the search yielded a successful match or not. The rest of the sub-components (column
andline
) would have well-defined values if thesuccess
istrue
, and undefined otherwise.Thus, our function might look something like this:
SearchResult search_text(const std::string& subtext, const Document& doc) { // --snip-- if(found) { return {true, column_no, line_no}; } else { return {false, random(), random()}; // random to simulate undefined value } };
Now, if we wanted to use our
search_text
function, being the responsible programmers we are, we would first check thesuccess
field:void perform_action() { // --snip-- const auto result = search_text("the jabberwocky", alice_in_wonderland); if (result.success) { printf("Found at line %d and column %d\n", result.line, result.column); } else { // !result.success printf("Unable to find given text within the document."); } }
This is definitely a solution, and one that was adopted in various codebases I’ve seen in the past, but it also suffers from a flaw, albeit a less severe one this time: the pollution of the codebase with many similar but oh-so-slightly-different types which served only as “rich-return-typed objects”.
This has the side-effect of an increase in the cognitive load of the programmer, which makes the experience of programming in that codebase a bit worse than it needs to be: the editors and other tools might help, but you still need to mentally keep track of which function returns what, and have some special handling around any of those.
Out-parameters
This one is a variation of the two themes above, falling somewhat in the middle: it still signals success and makes it harder (kinda…) to crash the application. This time around, our code would look similar to this (in Cpp-like pseudocode):
// Our SearchResult object struct SearchResult { int column; int line; }; // And the search function bool search_text(const std::string& needle, const Document& haystack, SearchResult& result) { // --snip-- if(found) { result = SearchResult{column_no, line_no}; return true; } else { return false; } }; // With our usage looking like this: void perform_action() { // --snip-- auto result = SearchResult{}; const auto found = search_text("the jabberwocky", alice_in_wonderland, &result); if (found) { printf("Found at line %d and column %d\n", result.line, result.column); } else { // !found printf("Unable to find given text within the document."); } }
This, again, also works. It’s a fine solution for many people (given how widespread out-parameters are) and enjoys a good performance profile because of the pointers/references, but I personally heavily dislike it from an aesthetics/philosophical standpoint:
A function should only ever return its return value and nothing else.
What can I say? I’m a functional-programming kind of guy.
Allow the return of a tuple containing the values
If we use the above quote as an axiom for our programming system, we kind of find ourselves in a bind: we both want to return a
bool
, and a secondary value based on the value of thebool
itself.Or are we?
For programming languages that only allow one return value, we can look into packing our
bool
and the auxiliary value into apair
or ann-tuple
(its arity-based generalisation).This used to be harder, but nowadays most programming languages offer more mathematical primitives in their standard libraries.
For example, adapting our example above to something like Kotlin:
fun searchText(needle: String, haystack: Document): Pair<Boolean, SearchResult> { // --snip-- if (found) { return Pair(true, SearchResult(column, line)) } else { return Pair(false, SearchResult()) } }
And for our usage, this time around:
val result = searchText("The Queen of Hearts", aliceInWonderland); if (result.first) { println("Found The Queen of Hearts at ${result.second}") }
There are also some languages that allow you to return multiple values in their base syntax (internally, they might do packing/unpacking automatically to achieve an effect very similar to the above). Of these, perhaps Go is the most famous one, using this as an idiom for error handling:
import "errors" func example_function(arg int) (int, error) { if arg == 42 { return -1, errors.New("can't work with 42") } return arg + 3, nil } func main() { result, err := example_function(42); if err != nil { // Log or do some error handling here } }
Option
/Result
/Either
Wow. What a trip. Let’s have a quick recap so we can review where we’ve been so far:
- We started with a function doing something, and returning
true
orfalse
to show whether it succeeded or not. - Our requirements evolved to now also need some extra information in addition
to the above
true
/false
values. - We explored a number of different ways to satisfy that new requirement.
But all of them were a bit lacking, in various different ways:
- They… allowed us to crash, either by accident or by misuse of their interface.
- They didn’t provide enough context to the compiler to assist us with development (and to provide guard rails against misuse), using pattern matching for instance.
- They depended on the creation of other context-sensitive, non-uniform types.
- They fallback to using types (e.g.
Pair
orTriple
) that lack specificity to guide our expectations and intuition to a specific context.
All of the above are solved by the
Option
andResult
type (and its more general dual in Haskell,Either
).All of them are what’s known in Programming Language Theory as Sum Types (sometimes called Discriminated Unions), due to the fact that the value set of the compound type is the sum of the cardinality of the sets of the atomic types (called constructors) that comprise them.
The type definitions look like these (in an ML-like pseudocode):
type Option<T> = | Some<T> | None type Result<T, E> = | Ok<T> | Err<E> type Either<L, R> = | Left<L> | Right<R>
All of the above are generic types, admitting generic parameters (the
T
,E
s, etc.).(At this point, it’s worth observing that
Either
is isomorphic toResult
, so from now on onwards, what we mention forResult
will apply toEither
as well, without the need to explicitly say so).
An interesting pattern arises if we also provide a type definition for a
Boolean
type in the same pseudo syntax:type Boolean = | True | False
If we also remove the generic parameters from the above type definitions (just for illustration purposes) and lay them all next to each other, we’re going to observe that their value sets their definitions are identical (with only different names for the various constructors):
type Boolean = | True | False type Option = | Some | None type Result = | Ok | Err
Wow! This near-identical (isomorphic) form is also a hint that their semantics are also very similar. Let’s try to put the generic parameters (but as comment for a clarity) and see how we do:
type Boolean = | True | False type Option = | Some -- with extra information | None type Result = | Ok -- with extra information | Err -- with extra information
A clear pattern now emerges! All of these signal the same sort of binary outcome, with the capability of also carrying extra information.
Nice! The keen-eyed amongst you will have noticed that this insight has now solved our original problem in a very clean way, and has at the same time provided a very clear usage guideline:
- Use
Boolean
whenever you only need to discriminate between two outcomes. - Use
Option
whenever you want to do the above, but also want to carry extra information around in theTrue
case. - Use
Result
whenever you want to discriminate between two outcomes but also want to carry extra information around in both of these cases.
Let’s have a look at some examples in Rust to see how we would choose which one to use in practice. Let’s have a look at 3 different cases:
struct Person { age: u8, } impl Person { // A Person can either drink (is of legal age) or not. // True or false. No other information needed. fn can_drink() -> bool { age >= 18 } }
In the above case we see an example of a predicate (a function returning
true
/false
, in effect solving a decision problem). Boolean values work very well for predicates: we only want the first order value - it’s eithertrue
orfalse
, but we don’t care why in either case.Let’s now have a look at a case where we might care about extra information in addition to the first-order value: Let’s assume we’re searching for a value in a vector and want to know its index in it:
fn search(needle: &String, haystack: &Vec<String>) -> Option<usize> { // Return `Some(index)` if value is present `None` otherwise haystack.iter().position(|x| x == needle) } fn main() { let fruit = vec![ "banana".to_string(), "mango".to_string(), "apple".to_string() ]; let res = search(&"apple".to_string(), &fruit); assert_eq!(res, Some(2)); }
In this case, our search function will search for the string
needle
inside the collectionhaystack
. If it succeeds (thetrue
case), it’s going to return us a value that signals that it succeeded, along with the extra information we asked for (the index in our case). This is theSome(T)
constructor that we saw above in the type definitions. If it fails, it will instead returnNone
(thefalse
equivalent).This is a great example of the typical use-case for
Option
: we use it in contexts where we want to attach auxiliary data in the successful case, but where if we fail, we don’t care enough to know more about the failure case - only that it happened.That gap is being filled by
Result
- it’s likeOption
semantically, but it allows us to also carry around extra data in the casefalse
case. This is useful in that it allows us to carry around diagnostic information in the failure case, to allow with error reporting and recovery.An example of this need being satisfied would be the following function:
fn read_to_string(filename: &str) -> Result<String, io::Error> { let mut file = match File::open(&filename) { Ok(f) => f, Err(e) => return Err(e), }; let mut text = String::new(); match file.read_to_string(&mut text) { Ok(_) => Ok(text), Err(e) => Err(e), } }
Here our
read_to_string
function is doing two things:- Open a file, and
- Read its contents into a string buffer.
Any of these two operations can fail. If we only had a
bool
value as the return value, we could signal that a failure happened by returningfalse
, but we would be at a loss as to what has actually failed.In this case, our
Result
type allows us to do the following:- If everything worked fine, return the
Ok()
constructor with an appropriate value attached (the string buffer in our case, after completion). - If there was an error, either at file opening or reading, returning an
Err()
constructor, with an appropriate error message attached.
Conclusion
To wrap everything up in a TL;DR form, here’s my usage heuristic for the above types:
- Use
bool
for simple predicates. - Use
Option<T>
when extra data needs to be carried in the success case (for instance when searching for values in a collection) and the failure case is signaling a simple absence of value for any reason. - Use
Result<T,E>
when extra data needs to be carried around in the success and failure case (say, if you’re reading something from the network, and you want to return the data in the success case or an appropriate error message in the failure case).
-
Yearly Goals: 2023
Context
In a previous post I promised a post on my yearly goals, along with the rationale behind them.
The reason I have explicitly outlined goals every year is that they serve in the same capacity that OKRs serve in a corporate setting, that is, to provide a northern star during the time period they demarcate along with some quantifiable metrics that can provide a final outcome analysis.
I started setting explicit yearly goals in 2020, and the results have been great for me, in the sense that while I don’t always reach my goals, the documentation itself serves as a clarification and reinforcing mechanism for my thinking process and my short-term and long-term goal-setting.
So, without any further ado, let’s move on to the…
Yearly Goals: 2023
Main Themes:
I have organised my developmental goals for 2023 around three main themes:
- Improvement of financial stability
- Improvement of mathematical maturity
- Improvement of musical capacity
These are the main development themes - I do maintain some side goals in my plan, but I try to build around a certain number of development themes every year. Main themes within this contexts means that the majority of my efforts for the year is going to be spent in developing across one of these main themes.
Let’s see what I aim for in each of these development themes.
Financial Stability
Last year was a very exciting year for me personally, with the biggest change being my purchase of a new appartment in Oxford. I have gone through all of the benefits that this move afforded me in a previous post, but this move also came packaged with significant financial strain in the first few months after the purchase. It also didn’t help that most of my financial resources went into the purchase itself (and associated costs like conveyancing, etc), so that meant that I found myself earlier last year with significant liquidity constraints.
These liquidity constraints have significantly eased throughout 2022, as most of my furniture has now been paid off and cash flows are getting freed to serve other purposes, which allows me to focus on the following two subgoals for this year:
- Enhance my financial safety net, and
- Support long-term financial outcomes.
Both of these two subgoals are measured against specific amounts levels that my L1 (liquid), L2 (support + growth) and L3 (long term growth + illiquid) positions should have achieved by the end of the year.
The reasons for these two subgoals should be fairly obvious:
- A robust financial safety net would allow me to deeply focus on my other activities without financial constraints occupying mental space (or at least, significant amounts of it), and
-
I recently read a book on retirement investing, which had some benchmark figures of where retirement accounts should be by certain age groups and what percentage of your income you should be saving for retirement at certain age categories, and shockingly, I found out that I am quite a long way away from these benchmarks in my current capacity.
Long term investing is the kind of thing where small changes today can compound into massive differences between outcomes in the future, so for now I have decided to allocate an additional 3%+ per month of my total income towards enhancing my retirement accounts and increase the chance that future outcomes are more aligned with what the experts suggest is a viable retirement strategy.
(This allocation strategy is just for this year, and I intend to revisit this decision at the end of this year to checkout how it has performed and whether it does indeed help with getting closer to the outcomes I am looking for.)
Mathematical Maturity
Coming out of university I believe I had attained a very robust background in Discrete Mathematics, which at the time aimed to serve very well with my then chosen subdomains in Computer Science: Compilers, Languages and Verification.
However, I didn’t practice (both at university and afterwards) my maths skills in other subdomains, to the point where I have found some my skills there atrophied.
I had made some efforts during 2020 and 2021 to repair my skills there, but as I expand across multiple domains, I have found that an enhanced mathematical capacity will be a significant asset for the rest of my life (and would support my future forays into data science, quant finance and synthetic biology a lot better) - with the added bonus of building competence and confidence that would allow me to be fearless against any mathematical problem that I face on a going forward basis.
To that end, I have decided to dedicate a good chunk of my time this year to improve my mathematical maturity. I have some study guides that I am following, the most significant of which are:
I do not intend to follow these too closely for now, as I want to have my exploration be curiosity driven - but certainly having some study guides helps with having guidance if and when I need it.
Musical Capacity
I hold no secret of my musical aspirations (I will become the male equivalent of Ava Max… eventually :) ).
I have been moving slower than I would like to on this front, so this year I have set up a target hours-spent goal of about 250 hours spent developing across anything that would enhance my skills in this area. This includes but is not limited to:
- Dancing,
- Songwriting,
- Music Theory,
- Piano,
- Guitar,
- Singing.
Regardless of the time spent building across these, I still don’t expect that my skills in this domain will be deployable in any meaningful capacity, but I do expect that a certain number of hours in which I deliberately practice should help in slowly chiseling a core set of skills that I can depend on at a later date.
Last thoughts
A pretty tall order this year as well with regards to yearly goals, but I am confident that these should be pretty attainable.
All of these are iteration based, so they just depend on my habits, which are pretty streamlined at this point. The goals have been crafted in such a way as to be controlled by me and appear resilient to uncertainty, as they do not depend on external sources or any massive action nor are there any clearly identifiable risks on the horizon across any of these development themes that would threaten my yearly desired outcomes.
Wishing myself and others a beautiful and productive 2023, with all your goals achieved, leading us all into a brighter future.
-
A Year In Review: 2022
Progress Review
Another year has come to a close, and so this time I decided to try a new experiment - I’ll author a public review of my year.
The yearly review is not a new concept for me (if you recall from hints in previous posts, I’m hugely into quantified self and various metrics and reflection processes). But a public one (sharing where I am in real time) is new.
The experimental side of it is based on the fact that by laying my progress bare, I hope that I can connect with other people on a similar journey, or at the very least provide a template for other people who may be inspired to set out on a similar journey (I have been inspired by others in various subdomains in the past, and hope to pay it forward this way).
I will run through this in the same way that I go about my usual yearly reviews, enlisting various accomplishments into various buckets that I call “progress subdomains”, which are various domains in which I want to broadly improve (and which align with my long-term vision for myself).
I will also be providing some background as we go through the various buckets, to help contextualise both the aims and the activities surrounding these buckets.
Highlights
- 1409 total self improvement hours, new yearly record
- 78 books read during the year, new yearly record
- 4 new programming languages engaged with
What went well
- Significant progress in self-improvement capacity, afforded by the greater focus
allowed by living alone for the first time in a while in
o2.Flutter
. - On the same note, managed to clock in
1409
hours of self improvement, against1290
hours in 2021, which was already a record year on its own. - Significant time spent on improving CS skills, while also improving rapidly across Biology/Chemistry.
- Uptake of musical capacity, spending significant time on practicing singing, guitar, piano and learning music theory.
- Reinitiation of my dancing training - this time through Steezy
- Furnishing
o2.Flutter
is 95% complete. - Improvements in financial posturing throughout the year.
- Delivered above expectations at work, setting expectations for promotions/salary increases in 2023.
- Recommitted in Q1/22 to improve my Spanish knowledge, and managed to clock in over 50 hours of study during the year.
- Improved processes for managing my time, including running my personal life in terms of sprints.
What didn’t go well
- Acquisition of R&G facility
o2.Flutter
had initially caused significant financial strain, applying a lot of liquidity pressure in early 2022, and acquiring significant debt load in attempt to rapidly furnish it. - Financial markets turmoil materialised after the closing of the acquisition of the R&G facility, but this meant that while I was spared the brunt of the market decline marking losses on my assets, I also didn’t go very far in terms of net worth growth, as asset prices declined while I was committing more money in the markets.
- Most of my acquantainces leaving Oxford during the 2020 pandemic meant that I now find myself with a significantly diminished (in terms of size) social circle in Oxford. This hasn’t presented itself as a major challenge yet, and some measures have been taken to ameliorate any future risks here, but I’m mindful of it.
- I’m not a huge club person, but I appeared in one earlier this year, begrudgingly
brought by some newly acquired set of acquaintances. I was a stick - unable
to move at all. This was unacceptable for two reasons: 1) I spent 3 years doing
latin dancing very dilligently, and 2) I intend to become a pop singer in the
future and dancing is a huge part of stage presence.
- (This was what prompted my getting into dancing again as highlighted in the list above.)
In Greater Detail
o2.Flutter
In late 2021, I closed on a transaction to acquire a new appartment in Oxford, to use as my new base of operation (work from home, practicing music, studying, etc).
Bases of operation are technically categorised as Research & Growth Facilities by me (because I’m a nerd, and because I like to mentally picture them as labs/facilities for improvement rather than a place for being chillaxed and tardy), so this one has been symbollically codenamed
o2.Flutter
.o2
is a nod to this being the second place in Oxford that I expect to spend quite some time in, andFlutter
is symbolising that this is going to allow me to fly higher. :)After closing, I quickly flew to Greece for some administrative work there, which meant that I moved in when I came back some time in early 2022.
This was easily one of the best moves I’ve done in my life thus far, for a variety of reasons:
- I’m a bit unusual (in lack of a better word) in a lot of ways, which means
that it made it harder for me to live with others in a houseshare.
- This had become such a big issue for me, as I was in my late 20s and living with students in houseshares, who… have a different lifestyle. This means that living with them as a working professional, especially one working from home is untenable, to the point where I had an internal deadline of solving the housing problem before I hit my 30 year birthday or I would leave Oxford (and potentially England) for good.
-
The appartment is a new-built, with all the goodies these come with, and especially: soundproofing. This means that I can practice my music without bothering anyone.
-
I have a room dedicated to being the home office. This is a huge part of what I want from my R&G facilities going forward. It ended up being amazing in synergy with the fact that both the company I work at and the industry at large are moving towards a more permanent Work From Home regime.
-
It’s also great in that compared to the work office, I can zone in and become focused on a task at hand at a much more rapid pace, skyrocketing my productivity both at work and at self-improvement endeavours, like reading for information.
- It’s also great to have an enclosed space which I can shape up however I want,
and can apply my standards to. No more kitchen in a mess because someone else
cooked and couldn’t be arsed to clean up because he had to play some video games :)
- This fact alone has contributed massively to my general lack of anger and tranquility that I enjoy these days.
- And, perhaps most importantly, the lack of distractions/commuting means that I can spend unreal amounts of time engaging with self-improvement activities (practicing instruments, reading books, practicing programming, working out at home, etc), that is further compounding my general level of happiness/direction/fulfilment.
Of course, buying a house, especially in Oxford, comes with a whole swath of new challenges, including:
-
There’s a significant financial outlay in the beginning. Not only for the property itself, but paying conveyancers, mortgage arrangement fees, taxes, etc.
-
There’s also the problem of furnishing the place. It’s expensive. It’s even worse if you have to furnish a new (i.e. empty) house against the backdrop of a 10-year high inflationary spike, and severely disrupted supply chains (most of furniture ended up being delivered with a significant delay).
-
To afford a place that’s close to my specification for an R&G facility, I had to buy somewhere at the edges of Oxford, which means that the few times when I have to commute, commuting takes much longer now.
-
It’s also generally much more expensive having a house - I’m now financially responsible for everything here (all bills, potential damage or substitution of devices, etc), which is something you don’t have to concern yourself with as much when renting.
Overall, the biggest challenge is that owning the facility itself has increased my outlay for housing costs by about 2x, but of course, for me personally, it was worth it (and then some) if I take into account all of the new capacity for self-improvement that it affords me.
Intellectual Progress
It’s hard to quantify my improvement in terms of intellectual prowess, seeing as it doesn’t lend itself easily to any sort of quantification, at least not one that is broad within different subdomains (I can’t say I became
x %
better in Chemistry this year with accuracy).But what I can do (and what I actually do) is measure what I call mini-achievements, which are more or less what it sounds like: micro-checkpoints across the larger roadmap, which show me that while I’m not there yet, I’m moving in the right direction. The thinking behind this is that after accumulating a lot of those within a subdomain, I should have enough obvious progress (say, if you’ve read a number of books in Chemistry, in a focused and mindful way, over time your Chemistry knowledge is bound to accumulate).
Programming/CS
Let’s start with my primary subdomain of focus: Programming/CS. This is because I spend a relatively large amount of time reading/practicing/improving my skills in Programming every year - well, it is how I make the majority of my living these days in any case.
(I lump Programming/CS in the same bucket, not because they are the same, but because they fall within the same self-improvement bucket for me, so I’m studying both interchangeably enough that I can consider them one thing).
In 2022, I spent about
347
hours studying CS, representing about 25% of the total time spent self-improving in 2022. In that time, I did the following:- Engaged with four new languages during the year, Ruby, Clojure, Haskell and Rust.
- Parts of this work immediately paid off - I engaged with a relatively big
piece of work in Rust at work, which was to build to expose a Rust-based
API for CBMC. The first PR is at cbmc#7410.
- I’m especially proud of my work in this project - the code represents my early understanding of Rust, which I’m bound to look back over time and cringe, but it was FFI work (i.e. very hard to debug) in a language that I had a cursory understanding of, delivered in a very tight deadline (less than two sprints).
- Parts of this work immediately paid off - I engaged with a relatively big
piece of work in Rust at work, which was to build to expose a Rust-based
API for CBMC. The first PR is at cbmc#7410.
- Improved my understanding of testing, diving deeper into TDD and Property-based testing.
- Improved my understanding of functional-design and object-oriented design, picking up on a couple of software-engineering and software-architecture books.
- Worked on a few compilers/interpreters written in a variety of languages (Go, F# and C spring to mind).
Chemistry/Biology
My long term aspirations is to found a biotech company. To that end, in 2022 I took big action towards advancing my scientific understanding of Biology. That was accompanied by a lot of time spent studying Chemistry as well - the fields seem to have a similar relationship to each other as do Computer Science and Maths.
In 2022 I spent about
166
hours studying Biology and Chemistry (about 12% of total self-improvement time) and in that time I:- Attended a synthetic biology course by MIT,
which forced me to read and pick up a lot on molecular biology/chemistry at an
accelerated timeframe.
- In the course’s final project, I focused on immunology, which is another subfield of biology I ended up being fascinated of. Expect to hear more on this from me.
- This was accompanied by a lot of hours spent on Khan Academy’s Chemistry/Biology
courses, as well as on Youtube, watching educational videos such as the ones by
the Amoeba Sisters.
-
As an aside, up until last year, I underestimated the value of Youtube, having it associated with music/gamers/memes/lolcats in my mind, thinking it to be worthless for any serious pursuit.
Turns out I was wrong, and there’s some serious content creators there that make your life significantly easier as you try to wrap yourself around some complicated concepts. I wish I had discovered that earlier, though there’s only myself to blame in this case.
-
Economics/Investing
I started self-studying Economics and investing when I first moved to England, some 5 years ago. I started studying it at that point because I felt like my future job aspirations (Biotech founder) couldn’t be fulfilled without at least a rudimentary understanding of economics, as I felt of a company as an entity not existing within a vaccuum, but as something existing within a larger context, the economy.
Years later, while I definitely feel that it’s true that a company is tied to a macroeconomic context (much like Closures in programming are bound to the enclosing environment), I’m not sure how much of an economics science understanding a founder actually needs - but it turned out great for me personally, because I found the way of thinking that economics promotes to have great synergy with my CS/engineering education, along with how I like to think naturally.
At this point, I feel like my understanding of Economics is very robust (I would like to think that I’m approaching, if I haven’t surpassed, the understanding of a Economics undergraduate).
Despite this, I continue reading books and articles (which I haven’t been dilligent in tracking, to be honest) in an effort to make my Economics mental models be more robust.
Of course, it’s hard to study Economics without being at least made aware of a sister field, Investing. This is my second love, in par with Economics. Because of their close relationship, and how I utilise my knowledge within these fields in my life right now, I consider them to fall within the same bucket.
- Got a number of books read on Investing/Economics this year.
- Read an innumerable amount of articles on various media forms (papers, blog posts, etc).
Spanish
Part of my self-improvement regime demands that I learn new languages, for a number of reasons:
- They help me become a more cultured and sophisticated person,
- They build me into a more round and multifaceted person instead of being a one-trick-pony.
- Being able to speak multiple languages is an emblem of prowess in the self-improvement circles - compared to other more superficial things, like say, wealth, noone can gift you a foreign language. You have to put in the work to learn it. Hundreds of hours of it. This (to me and others) commands a lot of respect, and generates a lot of confidence and self respect.
I had started with Spanish some years ago, primarily because my native tongue is Greek and Spanish is very close to it, both from a phoneme (people mistake me for a Spaniard when I do speak the little that I speak) and from a grammar standpoint.
This past year I committed to take it further, and I started learning Spanish again sometime in Q1/22, and took the habit across all of 2022, spending a total of about 57 hours total (about 4% of total) learning in a variety of ways (primarily through Duolingo, but also using books and going to language exchange meetups.)
Mathematics/Philosophy
These two don’t fall within the same bucket for me from a self-improvement standpoint, but I list them this way here because I’m at a point where I’m studying these purely from an intellectual standpoint (I don’t do philosophy in any professional capacity, and I appear to know enough Maths to get by professionally - though I’m always keen to learn more.)
- In both of these areas, many books/papers/exercises were read/done.
Music
I keep no secret that one of my “side quests” in life is developing my musical capabilities to the point where I could pursue a career as a professional musician.
This was a dream of mine ever since I was a teenager, and even though I was taking singing lessons in the past, I never looked at music seriously enough as a pursuit.
That was, until about 2020 and the beginning of the pandemic, when I bought myself a small space piano just as the initial lockdowns were announced in the U.K.
Ever since I have been spending a significant amount of time in developing my music skills every year (about 20% of total), but in 2022 and going forward, I want more time spent so that improvement is coming at an accelerated pace.
Highlights for 2022 include:
- Establishment of more consistent practice habits for guitar and singing.
- Continuation of my piano practice.
- Deeper diving into music theory and a small initial foray into composition.
Fitness
In terms of Fitness, 2022 was an about average year. No significant weight swings, with my mass being in the range of 63-64kg, and I managed to keep a relatively lean physique, of about 13-10% body fat ratio throughout the year (with a kilo gain between some holiday periods, when I let myself loose for a week or two).
In terms of the average day, both my diet and my fitness regime can be characterised as robust, or at least adequate for my long term vision. Despite this, however, a small effort had been put in enhancing my understanding and knowledge around sports science and nutrition, with an aim of bringing a small improvement on a going forward basis.
Given that my regime is robust as it stands, any improvements are not directly observable, not at this point at least.
Financial posture
I opened 2022 having just closed (at the end of Dec 2021) a significant transaction to acquire R&G
Flutter
.While this allowed for a number of benefits (as I have outlined in previous paragraphs), it also had an unfortunate side effect of requiring massive capital outlays (at least for my finances) and depleting liquidity sources, getting me to experience significant cash crunches in early 2022.
Thankfully those subsided over time, as I adjusted to the new cash outflow regime, and my cash flows improved due to a variety of reasons (raises at work, U.K government energy subsidy-support, etc).
However, I also had to finance some furniture which ended up requiring significant percentage of my cash flows into servicing debt in 2022. This has now subsided somewhat, primarilly because the brunt of it has been paid, but there’s still some long term debt that’s not completely of my books yet, and probably won’t be for the next 2-3 years.
I will also remiss not to mention that I did actually observe the inflationary spike eating a bit into my discretionary income, around the summer time and onwards, which added some further pressure.
In terms of other portfolios I had, despite having liquidated the majority of my holdings to acquire
Flutter
, there was still exposure to the market conditions from some portfolios that I can’t touch (say Workplace Pension, etc) and some illiquid portfolios (private holdings through Crowdcube for instance), I was still a bit affected over the year in terms of total net worth, which for the year remained flat (under most reasonable assumptions/mark to model).This was a bit disappointing, as my finances are a relatively chunky picture of my life, and one that I’m personally very proud of, but it would be naive not to expect some down years, and to be completely honest, I have to be thankful that it remained this way and didn’t experience significant losses.
On the plus side, the lack of progress on a financial front was counterbalanced by my progress in other areas, so there was no psychological impact on me as well.
2022 was a colourful year as far as my financial posturing is concerned, with the following highlights
- Got depleted liquidity sources to be back up to a level considered adequate.
- Got to the point where I can start outlaying some cash for investments again.
- Started contributing more (%-wise) of my income to pension accounts.
- Started directing future investments into cash-flow producing assets, with an aim to medium/long term have a dividend portfolio.
Conclusion
This has been a long one, and I probably have to cut it short somewhere around here, to preserve the interest of any reader and not bore them with details about my life.
All in all, I would call 2022 a fairly productive year, despite some of the headwinds observed in some of the progress subdomains.
I will probably author another post laying out my aims for the year ahead.
For now, so long, and wish both you and me a most productive/successful 2023.
-
A Couple of Mental Models to Help Us Tackle Software Complexity
Have you ever started designing your software while doing your best to keep the design clean and simple, and still came up with a tangled mess? Read on for a small number of mental models that might help you avoid this next time.
Everything’s in your head: A small introduction to Mental Models
Excited yet? Me too.
But before we go on further with our discussion, let me introduce an appropriate definition for Mental Models, just to make sure that we are all in the same page.
Mental models, as their name suggests, are abstract mental representations of a wide variety of topics. On a daily basis, we use mental models for everything: how we understand certain events that happen around us, how we expect a certain object or the environment to respond in our interactions with it, and a lot of other things.
They were first introduced by Charlie Munger (of Berkshire Hathaway) fame in a talk he gave named A Lesson on Elementary Worldly Wisdom.
For more reading on Mental Models, I refer you to the excellent guide by Farnam Street, which played a large part in popularising the idea of mental models (and familiarising me with many of the ideas).
Mental models and programming
In programming (and computer science, more generally) mental models are everywhere. Sometimes they are explicit (for instance, Models of Computation are abstract representations of how humans or machines can perform a computation). Other times, less so.
As an example, your knowledge of your favourite programming language is really a just a model of it. Think of Python - you don’t really know Python.
Now, I can already picture you recoiling in disbelief. “How is that possible?”, you ask. There’s something in your head that allows you to write programs in Python, so my claim looks ridiculous upon surface examination.
Not so fast - I didn’t finish 😁
You don’t know Python - but what you do have in your head are two (partially?) complete mental models of Python: a mental model of its syntax, and a mental model of its semantics. You may also have mental models of the behaviour of certain Python tools. You might also be very familiar with a number of different libraries (e.g.
requests
), at which point you may have a very robust mental model of its behaviour or the internal mechanisms that drive that behaviour. You get the point.Our first model: The Function as an (Invisible) Machine
In the two most well established models of computation the smallest unit of code organisation is the function.
We software engineers deal with the abstract - we design software (write functions), but for the most part, we see the end result of the computation they perform and not much more. You can think of a function as an ethereal object (I stole this analogy from Structure and Interpretation of Computer Programs, where the authors compare it to an “abstract being that inhabit computers”).
If we take the previous analogy, and ground it a bit into the more concrete idea space, we can think of the function as the ethereal equivalent of a machine. It’s really the digital equivalent of the machines other engineers are dealing with: our machines have some inputs and are comprised of a different number of components, the interplay of which produces outputs.
(This view aligns very well with ideas from the field of Software Engineering as well.)
If we admit that the analogy holds, then we are met with some pretty interesting implications. For example, our ethereal machines share the following properties with concrete machines:
- Complexity,
- Failure modes,
- Interfaces,
- Etc.
Simple machines - Complex Machines
I’m going to stray away from computers for a moment so that I can make a point.
Think of a simple machine, say a Bicycle. A bicycle has some components (say, it has a frame, tires, pedals, a handlebar, etc.) and performs a function, namely, getting you from point A to point B.
There are other machines that are functionally equivalent from a high-level standpoint, for example a motocross bike. These machines however are lot more complex: they possess a greater number of components and features a greater number of interactions between these components.
(Now, of course, the motocross bike offers functionality not present on the bicycle, but that is beside the point I’m trying to make here.)
It should not be that much of a logical leap that the more complex machine (with many more components and interactions) has more failure points than the simpler machine. Not only that, but in the case of the simpler machine, it takes less knowledge and effort to fix any failures.
Let’s hold that thought for a bit while we have a look at the second mental model I wanted to talk about and go on to see how these two interplay with each other.
Bounded Rationality
Enough talk about the machines - let’s now have a look at the human side of all this: we are all affected by a cognitive bias known as Bounded Rationality.
Bounded rationality is effectively a £10 word for describing the phenomenon of some cognitive limitations, such as:
- the lack of perfect information,
- the limited capacity of our short term memory,
- the collapse of long-winded reasoning processes under a time limit or an informational overload,
- the interplay between other cognitive biases, etc,
negatively affecting our decision making process. The way the do that is by shifting our decision making priority to be that of satisficing (aiming for just an adequate outcome) instead of optimising (aiming for an optimal outcome).
This means that instead of going for the best solution in the current context, we just go with the first one that seemingly satisfies the constraints that are obvious to us at the moment.
You will see bounded rationality being discussed in different contexts. It is, however, especially prevalent in behavioural economics (the field of study combining economics and psychology), and cognitive science. The concept is more general, though, and is relevant in any context that entails decision making.
When it comes to us, for the purposes of this article, we care about how bounded rationality affects us when we design software, and this is what we’re going to be focusing on from now on onwards.
Bounded Rationality and Software Design
At this point, an astute reader should be able to see where I am going with this:
A function should be as simple as possible so that we can avoid the pitfalls associated with cognitive limitations on our end.
You may have seen software design guidelines before that look like this:
- “Limit the number of parameters a function accepts to less than 5”
- “Do not use global variables”
- “Use preconditions and postconditions”
- “Obey the Law of Demeter”
- …
All of these, from a high level standpoint, aim to narrow down the size of the set of components and interactions of our function - our (invisible) machine.
Indeed, programming languages today include a number of features (static manifest typing, lexical scope, etc) that limit the input, range of states and behaviour that our code can find itself in.
This has a number of benefits relating to software correctness (as the compiler enforces these constraints) but the biggest one is probably that it frees up our mental capacity by making the function simpler. A smaller design space for that function means less things that can cause us to throw our hands up and start satisficing, and also less things that can go wrong or need fixing when we inevitably make a mistake.
Conclusion
I hope that my small essay convinced you about the need to make functions as small as possible (but not smaller 😁 ).
If you want a TL/DR of the whole article, it boils down to this:
The simpler your software design, the less likely your cognitive limitations are going to negatively affect it.
-
Three (plus one) Different Versions of map in Haskell
If you are here, then
map
requires no introduction. You have seen it. You have used it. Many times. You have even implemented it before. But did you know that there are more than one ways to implement the functionmap
?
Of all the archetypal higher-order functions that I know,
map
is the function that I hold dearest in my heart. That’s because it marked a very pleasant memory when I finished The Little Schemer (for the first time, back in 2016) and I found out that I grokked the concept enough to be able to write it on my own.As a quick explainer, for the sake of completeness,
map
is a function that takes two arguments, a functionfn
and a listlat
, and produces a new list with the elements being the result of the application of the functionfn
to the elements oflat
(in mathematical writing, this would be∀x ∈ lat, fn(x)
- that is, for allx
that belong tolat
we take the result offn
applied tox
,fn(x)
).I always knew of the (classic?) way to define that in
Scheme
(or more generally, inLisp
), which I had seen also being used inOCaml
/SML
, which looks like this:(define (map fn lat) (cond ((null? lat) '()) (else (cons (fn (car lat)) (map fn (cdr lat))))))
That is, a recursive function definition, that does the following:
- If the input list
lat
is empty, it returns the empty list()
(we use that as the base forcons
structing a list of values), otherwise - It returns the list
cons
tructed from the application of the function to the first element of the list(fn (car lat))
and the result of the recursive call on the remaining list(map fn (cdr lat))
.
In OCaml, the same function would probably be defined using pattern matching, but the pattern is the same:
let rec map fn lat = match lat with | [] -> [] | h::t -> (fn h)::(map fn t)
Recently though, I started reading Graham Hutton’s excellent Programming in Haskell, as a preparation for some more advanced material I wanted to read that required Haskell knowledge.
In that book, I was delighted to find not one, not two, but three different ways of defining the function
map
in Haskell.Let’s have a look at the first two ways of defining
map
in Haskell, both defined in chapter 7 of the book.The classical recursive definition of
map
map :: (a -> b) -> [a] -> [b] map f [] = [] map f (x:xs) = f x : map f xs
The first definition is the one that is closest to what we described above for both Scheme and OCaml. It’s the (structural) recursive definition using pattern matching against two patterns:
- The empty list in the first pattern
[]
, which as before returns the empty list, and - A pattern of a list with at least one element, which we immediately de-structure
into a
head
andtail
component in(x:xs)
. When matched, it will build acons
(:
) of the result of the function applicationf x
and the result of the recursive call of the remaining list (map f xs
).
This definition is basically the exact equivalent of the OCaml definition above, with the only notable difference being the explicit function signature given:
map :: (a -> b) -> [a] -> [b]
This tells us that the
map
function takes two arguments:- A polymorphic function that maps elements of type
a
to typeb
(a -> b)
, - A list of elements of type
a
([a]
)
and as a result produces a list of elements of type
b
([b]
).(The
a
andb
above are called type variables, and are implicitly universally quantified, i.e. they read as “for all elements of typea
”).So far, so classic.
Nothing out of the ordinary here.
Let the show start: The list comprehension definition of
map
Later on in the book, we come across this definition:
map :: (a -> b) -> [a] -> [b] map f xs = [f x | x <- xs]
Wow!
This one packs a punch in terms of conciseness, but I find it very elegant and very expressive at the same time.
This definition is based in the syntax for list comprehensions in Haskell.
List comprehensions are a concise and convenient way to define new lists by manipulating elements of other lists. As an example, consider the following:
> [x^2 | x <- [1, 2, 3, 4, 5]] [1, 4, 9, 16, 25]
This is read as make a list of “
x
squared, withx
drawn from (|
) the list[1, 2, 3, 4, 5]
.Taking this into account, and back to our
map
function definition, our comprehension there:map f xs = [f x | x <- xs]
reads as “make a list of the results of
f x
, wherex
is drawn from the listxs
”.Beautiful.
Roll the carpet for Mr. Fold: The
foldr
definition ofmap
One of the coolest things the book opened my eyes to was the fact that we can use a
fold
in a generative fashion.I was well aware of
fold
from both Racket and OCaml before, but I always thought of that in terms of a generalisation ofreduce
- it never occured to me that I can use the accumulating function to yield a value in a generative recursion fashion, like a new list 🤯(Embarassingly, it now looks kind of obvious, and related to ideas I’ve been exposed to in the past - this one is related to the
collector functions
idea that the Little Schemer uses in chapter 8).Back to our definition.
This one is not given by the book - in fact, it’s left for the reader to define as one of the exercises. This is what I came up with:
map :: (a -> b) -> [a] -> [b] map f = foldr (\ x xs -> f x : xs) []
This definition is the one that left me most stunned and amazed with the generalising power of a fold.
What this definition tells us is that we define
map
as a right fold (foldr
) of a lambda that takes two arguments,x
andxs
, andreturns
the cons off x
andxs
. The last value we pass to thefoldr
is the empty list[]
, which is the value that is used as the base case.In order to understand what the
foldr
does, I find the following visual from Wikipedia to be very helpful:To understand the above visualisation, you need to understand that a list in Haskell (and most functional programming languages for that matter), is a
cons
of various values and the empty list. I.e. you can think of[1, 2, 3, 4, 5]
as1 : (2 : (3 : (4 : (5 : []))))
.With that in mind, what the
foldr
function does is that replaces thecons
(:
) operator with the function argument supplied to it, and reduces it all (folds the list) into a single value.(It also replaces the base case empty list value
[]
with the value of the last argument supplied to it. In our case, we are passing the empty list ([]
) again, which we are going to use as a base to build a new list of values on top of.)And here’s the trick - because the (anonymous) function we passed to it is building a new list (by applying the
cons
operator again), what we end up is a new list instead of just a single value!In our case, this means that if we had the list:
1 : (2 : (3 : (4 : (5 : []))))
after the application of the anonymous function we would have a new list, equivalent to
f 1 : (f 2 : (f 3 : (f 4 : (f 5 : []))))
🤯
Bonus round: a monadic definition of
map
Okay, this is a bit of a cheat because this is basically our first definition, except that the function and the return type are monadic:
mapM :: Monad m => (a -> m b) -> [a] -> m [b] mapM f [] = return [] mapM f (x:xs) = do y <- f x ys <- mapM f xs return (y : ys)
This is using the
do
notation to define a sequence of actions, but the actions themselves have a near 1-1 correspondence to our original map implementation:- First we assign the name
y
to the result off x
, then - We assign the name
ys
to the result of the recursive call on the of the list (mapM f xs
) - And the return the
cons
of the two values(y : ys)
How is this definition useful?
You may have observed that the return type is
m [b]
- a monadic list of typeb
.Consider the following: we want a function that converts a string into a list only if all of the string characters correspond to a digit, or fail gracefully otherwise.
One way to do that is to write a function to convert a single character into a
Maybe Int
:conv :: Char -> Maybe Int conv c | isDigit c = Just (digitToInt c) | otherwise = Nothing
With this definition, we can now use our function
mapM
like this:> mapM conv "1234" Just [1, 2, 3, 4] > mapM conv "123a" Nothing
Conclusion
I enjoyed writing this post quite a bit.
It’s a very humbling experience to be visiting books and seeing that ideas that you considered to be elementary and rather surface-level are a lot more nuanced once you start digging deeper into them.
It’s something I need to keep top of mind as I go forward as well.
This post also highlights one of the nice things of going for breadth of knowledge: exposure to a wider set of ideas. Had I only stayed in the Racket/OCaml realm, I probably wouldn’t have been exposed to this range of
map
implementations, for the reason that these languages don’t offer some of the language features that enable them, like list comprehensions.All in all, happy I went down this path, and looking forward to what’s next.
- If the input list