How to Develop Type Sense

Saravanan M
9 min readApr 7, 2024

--

In this article I’m proud to present you the term I (think I) coined recently — “Type Sense”.

This is not the weekly-typical-technical piece. It’s more of a reflection based on my 2.5 years of experience(relationship) with strongly typed functional programming languages like Haskell and PureScript.

Many of you might be wondering what “Type sense” means?

Type sense is a person’s ability to understand, relate, and connect types. (just replaced “numbers” with “types”)

Photo by Des Récits on Unsplash

Same as number sense, its a person’s ability in interpreting, creating, working around with types in a effortless way. You might have seen people who are able to just simulate compilers, run type checks, compose, unwrap, bind etc all in their mind.

How these guys are doing this? Can you become one too? While I wouldn’t call myself a type wizard, I’ve developed a decent type sense over the years. I believe I’m eligible to guide others on how to do it, by explaining my journey and approach.

1 . Go back to BlackBoard

Imagine someone shows you a code snippet that’s pretty hard to grasp, like this:

type Person = { name ::String, age :: Int}

filterPersonWithAge :: Array Person -> Int -> Array String
filterPersonWithAge persons allowedAge =
persons
# filter ((_ > allowedAge) <<< _.age)
<#> _.name

When you encounter such code, grab a notebook or get a marker and walk towards the black board and break the code down step by step(just like you how you dealt with leetcode when you were a beginner)

For example, my older self would’ve done something like

i) Dissect the code into its simpler form

I understand that you want to master point free style, reading fancy operators like compose, map etc. But believe me if you are beginner, they can really be intimidating and prevent you from learning the basics. so, it’s better to break them down into their simpler equivalents.

For example,

  • Replace operators with their function counterpart. i.e replace <$> with map, <<< with compose, >>= with bind, etc
  • Convert point free code to fully applied function etc.
  • Convert $ to parenthesis, and all other means of simplifying the code.

Here’s the simplified version of the earlier code snippet:

-- simplified version of the above code
filterPersonWithAge :: Array Person -> Int -> Array String
filterPersonWithAge persons allowedAge =
mapFlipped
(applyFlipped persons
(filter (\person -> compose
(\age -> age > allowedAge)
(\p -> p.age)
person
)
)
)
(\person -> person.name)

The good thing doing the above simplification is, you will also be able to develop a sense of how infix operators’s precedence/associativity works, how point free style is expanded etc.

Note: If you find the simplified code above still difficult to understand, you can go a step further and even change the flipped version of apply, map to their normal version (apply, map), or or any other changes that make it ‘simpler’ from your perspective

ii) Write down the function types.

Also note down the function signatures of all the functions being used,

filter :: (a -> Boolean) -> Array a -> Array a
map :: (a -> b) -> Array a -> Array b
applyFlipped :: a -> (a -> b) -> b
compose :: (b -> c) -> (a -> b) -> (a -> c)

Note: As you practise this particular ritual, you’ll find yourself internalizing(memorizing) these type signatures without even realizing it.

iii) Resolve the abc’s

Having obtained the generic function signatures from the documentation, it’s time to replace the universal quantifiers (forall a) with the concrete types. For example, let me update the type signature for the filter function:

filter :: (Person -> Boolean) -> Array Person -> Array Person

And make sure to do the same for the other functions as well.

iv). Evaluate the Function

After resolving the types, it’s beneficial to visualize how the functions interact and the flow of data through them. Creating a flow diagram or any diagram that aligns with your mental model of function execution can be highly beneficial.

Understanding even the trivial aspects, such as how the output from one function is consumed by another, can significantly enhance your grasp of types. (You can ignore this: From there, you can delve into more complex topics like how implicit contexts are maintained(State), how type specific behaviors like short circuiting happens(Maybe, Except etc), how types are layered at stack transformers, how instances are resolved by the compiler etc)

For the above code snippet, I would’ve drawn a flow diagram like,

                ┌───────────────────┐
│ Input │
│ (Array Person) │
└───────────────────┘

@Array Person


┌─────────────────────┐
⌌───────── │ Filter │◀──────────⌍
│ └─────────────────────┘ │
│ │ │
│ @Person |
│ | │
│ ▼ │
│ ┌──────────────────────────┐ |
@Person │ ┌───────────────────┐ │ @Boolean
| │ │ \p -> p.age │ │ ⎪
| │ └───────────────────┘ │ ⎪
| │ │ │ ⎪
⌎─────-▶│ @Int │────▶────⌏
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │\age -> age > allowed │ │
│ └──────────────────────┘ │
│ │ │
│ @Boolean │
│ │ │
│ ▼ │
└──────────────────────────┘

@Array Person


┌-------------------------------------┐
│ map (\p -> p.name) │
└-------------------------------------┘

@Array String


┌---------------┐
│ Output │
│(Array String) │
└---------------┘

The flow diagram comes in very handy for deeply composed expressions.

Note: As time unfolds, you can throw away your notebook because all this will start happening in your mind itself.

v) Add the complexity layers again

Once you’ve reached this point, repeat the first step but add a bit more complexity this time. For instance, in the second iteration, keep the point-free style or the <<< operator.

Keep on repeating this step until you are able to understand the code in its original form.

2. Make Use of Repl

Repl, in my opinion, is a great place to sharpen your type sense skills. While getting started I remember spending so much time in repl alone. In languages like Purescript and Haskell, you can open the REPL and ask for the type signature of any function or expression using :t.

For instance:

> :t map (append "hello" <<< snd) <<< (zip [1,2,3])
> Array String -> Array String

You can even start treating this as a game and try guessing what the type of a expression is. This acts as a stimulating puzzle for your type-evolving brain.

3. Type Holes

One of the coolest but underutilized features in functional programming languages like haskell, purescript etc are type holes. With this feature, you can ask the compiler to help you figure out the type that fits in a specific context. It’s like asking the compiler, “Hey, can you tell me what type should go here?” The compiler will even suggest functions that fit the type hole.

For example, consider our old function which is now type-holed

filterPersonWithAge :: Array Person -> Int -> Array String
filterPersonWithAge persons allowedAge =
persons
# filter (?wfh) --- ?wfh => type hole
<#> (_.name)

Note the ?wfh, which is a type hole. It can be any name prefixed with a ? (underscore in Haskell).

Now the compiler will give us some useful suggestions such as,

typehole suggestion from purescript compiler

Not only its saying that it wants a function that of type signature:

{age :: Int, name :: String} -> Boolean 

It also suggests some functions that fits in the hole (but not so useful in our case since our type, Person, is custom)

For beginners this can be a great tool, to play around with types, to think in terms of function signatures.

4. Pay some attention to compiler errors

One of the biggest mistakes, beginners do is they just don’t listen why the compiler is crying, they will just see the error and try fixing it by trial and error.

Consider you are working with a IO/Effect monad, and you have this below broken code:

import Effect.Console(log)
import Effect.Random(random)

-- random :: Effect Number
-- log :: String -> Effect Unit

add5 :: Int -> Int
add5 n = n + 5

main :: Effect Unit
main =
let r = random -- error
let res = add5 r
log $ add5 res

A typical beginner’s approach to fixing this code is as follows:

  • He sees the error:
Could not match type `Effect Number` with type `Int`
  • His first **instinct** is to remove the let with <-
  • Now the error is on the second line: He’s proud of his progress.
  Could not match type `Number` with type `Int`
  • He assume this error is also due to the let keyword and change it to a bind (<-) as well.
main :: Effect Unit
main = do
r <- random
res <- add5 r -- error
log $ res + 5
  • The cycle continues until they seek help from a senior developer, ChatGPT, or by themselves(they are working from home and there’s a powercut) after banging their heads against the code for an hour

If he had listened to the compiler’s message indicating a mismatch between Int and Number, he could have searched for a function that converts Number to Int, such as ceil or floor, and fixed the issue himself.

Just listening and trying to understand what the compiler has to say can save lot of times and is more effective in the long run, than mindlessly debugging with techniques like adding pure, toggling between let and bind, adding show etc which is a common pattern among beginners(I myself have done it🤣)

5. MindMapping

If you have done enough scribbling in your notebook, fixed far enough amount of compiler error(in a mindful way). The next step which happens by default is, you will start mind mapping the type signatures, catching of errors, composition of functions etc happens in your brain before it happens in the compiler. While this skill develops with practice over time, you can accelerate the process by deliberately practicing this type of mental mapping.

For this exercise, take any expression from your codebase, which you feel has quite complicated chain of functions and try to visualize how the data flows from one function to the other function in the composition chain, especially how the types are made compatible across the chain.

Like how you dry run a leetcode problem in your mind, try dry running the compiler for that expression in your mind.

6. Be Curious, Be Patient

I believe the primary driving factor behind how I developed the “type sense” skill is my fondness for types.

I can almost hear the collective sighs from those who detest dealing with types but are here to learn the secret sauce to gain mastery over them. If you are actually seeking the secret, then it is curiosity. (Are you still there?) Without a strong level of interest or curiosity, mastery can be elusive to attain. For some people, interest/curiosity often blossoms as their proficiency grows and a solid foundation is laid.

The master has failed more times than the beginner has even tried — Stephen McCranie

So if you are getting tired of looking at type errors, don’t stop.

With time and dedication(and your new found love for types) , momentum and mastery will naturally follow, igniting you with a passion for types. But be aware, this might lead you to search for category theory books in pirates bay, convince yourself that monads are the solution for world poverty, find yourself unable to write a single line of python code without pyhint.

I hope this article has helped cultivate a positive attitude towards types, which is the core motivation behind its creation. My goal is achieved if it has sparked your interest.

Additionally, if you have any additional insights or tips on honing the seventh sense — type-sense, please do share them in the comment section.

Additionally, if you have any additional insights or tips on honing the seventh sense — type-sense, please do share them in the comment section.

Disclaimer: If you find yourself relating a bit too much to the ‘coding mishaps’ in this article, and you happen to my colleague, it must be you only. But hey, I love the way you are 🤣

--

--

Saravanan M

A place where I share my perspective on the tech topics I've read and enjoyed. Sometimes highly opinionated.