
Haskell's kind system: a primer - ristem
https://diogocastro.com/blog/2018/10/17/haskells-kind-system-a-primer/
======
fmap
That's a really beautiful article!

Using kinds to keep track of the concrete representation of a _reference to a
type_ (rather than the concrete representation of the type itself) is one of
the real gems in GHCs design.

However, one problem with it is that a polymorphic type, such as [] of kind "*
-> *", or a function like map of type "forall a b. (a -> b) -> [a] -> [b]"
really only works with boxed types. I always wondered how expensive it would
be to monomorphize everything at the level of kinds and have what GHC calls
levity polymorphism by default.

Languages like Rust and C++ always monomorphize everything and while this does
get slow it is still not the exponential blowup that theory suggests. In fact,
there is an ML compiler (MLton) which does the same and it is still not too
bad. Doing it at the level of reference representation - which is where the
generated code actually needs to change - should be much cheaper and
definitely a practical default.

The only real problem I see is that you could in principle write code that
does "kind polymorphic" recursion, which couldn't be compiled away. But that
sounds seriously crazy and I see no reason to allow it in the first place...

~~~
Athas
> However, one problem with it is that a polymorphic type, such as [] of kind
> "* -> _" , or a function like map of type "forall a b. (a -> b) -> [a] ->
> [b]" really only works with boxed types._

Can you explain why this is, or provide a reference to literature where it was
investigated? I still (even after getting a PhD in compiling functional
languages!) do not understand why you could not create code parameterised over
the _sizes_ of the arguments. For example, a 'cons' cell could be represented
as first the size of the 'car' in bytes, then the 'car' value inline/unboxed,
then a pointer to the 'cdr' cell. The Sixten language[0] does this, although
it's still very early in its development. I'm not saying this is necessarily a
good solution, I just wonder why I have never seen it seriously considered
(and perhaps rejected).

In practice, I do believe that monomorphisation is very often a good strategy.

[0]: [https://github.com/ollef/sixten](https://github.com/ollef/sixten)

~~~
gpderetta
> why you could not create code parameterised over the sizes of the arguments

If I understand correctly, that's what BitC was trying to do, but the extreme
complexity of the compilation model was cited as one of the reasons the
project failed.

The postmortem is here I think, but I can't access it right now:

[http://www.coyotos.org/pipermail/bitc-
dev/2012-March/003300....](http://www.coyotos.org/pipermail/bitc-
dev/2012-March/003300.html)

------
divan
The article is exemplary – it's reasonably short, concise, beautifully
visualized and explains a complex topic with simple terms.

I'm confused with "type promotion" in the `ConnectionStatus` example, though.
When we promote `Open` and `Closed` from terms to types, we make it easy to
create a variable that can be either `Open` or `Closed`, but we're also
breaking the mental model of the whole thing. `Open`/`Closed` is a property –
not a "type" in a traditional way of thinking about types. For me, the type
should map onto some abstraction in your mental world map, and `Open`/`Closed`
is definitely not a good fit here.

How do Haskell programmers address this problem? Do they think about types
more in mathematical terms, rather than as tools for describing mental maps of
the problem domain?

~~~
tiniuclx
Open/Closed would still conceptually act as properties, but by promoting them
to types you enforce the property at compile time rather than runtime. Note
that they are not used on their own, but to describe a `Connection`. This lets
you statically enforce the fact that you must only send data through an open
connection, for example.

~~~
divan
Right, that's the obvious benefit for the compiler (statically enforcing
behaviour, etc), but I'm struggling with not seeing here a detremential effect
for the human.

To me, when you write code, you're encoding your understanding of the problem
domain (mental map) into the code, describing it in a way that permits to
restore it back. By reading a code your coworker or you 6 month later should
be able to restore the same mental map you had while writing the code. And
using type system to describe properties (there is another way to do it
already in place – fields in structs), seems like complicating this task and
it eventually should hurt readability.

~~~
almostdeadguy
The advantage is you basically don't have to think about the provenance of
data (in this example what state is a connection in? was it closed before
being given to me? etc.). It allows the compiler to ensure the user of this
code can't perform operations that lead to error states by allowing the
programmer to express the state transitions of a connection in their function
declarations, as the provided example shows:

    
    
      newConnection     :: Address           -> Connection Closed
      openConnection    :: Connection Closed -> Connection Open
      closeConnection   :: Connection Open   -> Connection Closed
      connectionAddress :: Connection s      -> Address
    

From a readability perspective, I would love this, it's effectively a state
machine diagram explained by your function types.

------
tiniuclx
I've been writing a lot of Haskell recently. I had to switch to embedded C and
the difference between the two was striking. The C program I was editing
compiled, but failed to do anything useful. To fix that, I had to reorder the
stateful functions and #define the right things without much help from the
compiler.

Had I been using Haskell and a reasonably well designed library, these
mistakes could've been caught at compile time, with the compiler telling me
exactly what I did wrong. The kind system described in the article is how you
would implement such a library, or something even more advanced.

Sure, comparing embedded C to Haskell is disingenuous, but it does show how
far programming languages have come. And I think my experience could
definitely happen in many other mainstream languages.

~~~
m_mueller
What exactly is it that stops Haskell from being used in embedded? GHC has an
LLVM backend, and LLVM should support most embedded architectures, am I
mistaken there? I have some suspicions of what it could be, but it would be
interesting to read your opinion/experience first.

~~~
tiniuclx
Sure, you could likely compile a Haskell program to an embedded architecture,
but Haskell is a garbage collected language. Allocating memory takes a long,
nondeterministic amount of time on a microcontroller. And microcontroller are
often used in control systems, where latency kills.

Haskell also suffers from space leaks due to its lazyness. Microcontrollers
have memory in the order of kilobytes, sometimes even less than that. You
simply can't risk running out of memory, which is why micros are programmed in
C or assembly, where you have a vice-like grip on how much RAM you use.

For a higher-level look at the problem, consider that embedded systems are a
high volume product. Saving a penny by using a cheaper, weaker micro adds up
very quickly when you're shipping tens, or hundreds of thousands of devices.
At that scale, it's profitable to spend more on non-recurring engineering
(programmer time) in order to reduce the cost of manufacturing.

If you want a functional-style language that is likely very well suited to
embedded devices, look no further than Rust. While I haven't had the time to
learn it (maybe next year), the language looks very promising due to its
memory model.

~~~
calebh
I've worked extensively with functional programming on tiny microcontrollers
like the Arduino. I created a research language called Juniper[1], which is a
Haskell/F#-like language targeting the Arduino.

As you mentioned, the memory size and the program size are the biggest two
constraints. There's no space for a garbage collector, and everything has to
be allocated on the stack. It turns out that it is possible to make the entire
thing stack-free. The two biggest hurdles are function closures, arrays, and
recursive data types.

Function closures are typically allocated on the heap since it's possible to
return functions from other functions. The solution is to include the closure
as part of the function type, which means that they can then be allocated on
the stack. Arrays are a bit more tricky to allocate on the stack. My solution
is to statically size arrays with type level natural numbers, which means that
the sizes are known at compile time.

Recursive datatypes are a problem that I have not dealt with yet. In most
Arduino projects recursive datatypes are not really used, so I just don't
allow them. It might be possible to use type level natural numbers to put a
bound on recursion depth. Another idea I had was to use run-timed size
datatypes. For example, let's say you have a function F1 which calls F2. The
frame pointers for their corresponding frames are at S1 and S2. When F2
returns a value of size N, the solution is to copy the value to a scratch
space, return to F1, decrement the frame pointer, and then copy the value back
into the stack frame. I'm not sure if this is really suitable for embedded
systems since it's a lot of code/machinery which takes up precious program
space.

Rust is a great language which can work around these issues due to its linear
types. I'm looking forward to using Rust once it starts to support the Arduino
microcontrollers.

[1]: [http://www.juniper-lang.org/](http://www.juniper-lang.org/)

~~~
naasking
Very cool! Hadn't heard of Juniper before. Do you use something like regions
to group and inline memory allocations?

~~~
calebh
In the next version of Juniper everything will be moved over to purely stack
based allocation. Currently only function closures and refs are allocated on
the heap. The refs are only created once when the program initializes itself,
so that isn't an issue. The closures will become part of the function type.
The FRP model works very well for the Arduino platform since you're
essentially modelling a data-flow from input to output devices. Currently the
compilation target is C++, which means that the Juniper compiler doesn't have
a ton of control over the final assembly code.

~~~
naasking
> The closures will become part of the function type.

Can you elaborate? Do you mean something like closures and function pointers
will be distinguished, with closures essentially being something like an
existential package paired with a function pointer? ie. ∃'x.'x * 'a -> 'b

------
MaxBarraclough
I had no idea Haskell used 'kind' to refer to something codified and concrete
in this way -- usually we see the word 'type' used to refer to the concrete
type-systems stuff, and 'kind' to refer to the fuzzier, less precise idea of,
uh, what it is the data _is_.

Type theorists say things like "Type systems let us reify what kind of data we
are dealing with".

It's a bit like when Java took the fuzzy term 'interface' and made it a
concrete technical concept, or when Apple added closures to Objective-C and
called them 'blocks', which was previously a fuzzy term.

Will we have to start using another word to refer to the fuzzier concept, if
not 'kind'? 'Sort' is already taken. 'Category' is already taken. Perhaps the
'nature' of the data?

~~~
lmm
Kinds are a distinct concept from types, and that concept needs a name, and
it's best to choose a name where the intuitive meaning of the name aligns with
the technical one (indeed Haskell attracts a lot of criticism when it uses
names like "monad" that _don 't_ have a "fuzzy" meaning).

It goes even further: types-of-kinds are called "sorts", though in Haskell
there's only one possible sort of kind.

There are only two hard problems in computer science, after all.

~~~
Intermernet
>There are only two hard problems in computer science, after all.

Including off-by-one errors, there are four.

~~~
MaxBarraclough
Maybe we should add 'induction' to the list.

------
ainar-g
This was the clearest introduction into types vs kinds in Haskell that I have
ever read. Definitely going to recommend to my friends. Are there other good
articles by the author?

~~~
rnavarro
His typeclasses article is also fantastic:
[https://diogocastro.com/blog/2018/06/17/typeclasses-in-
persp...](https://diogocastro.com/blog/2018/06/17/typeclasses-in-perspective/)

------
calebh
Once you understand kinds, it's not so hard to understand typeclasses and
monads. Type classes are a way to overload functions in a controlled way, by
defining a set of function signatures where the overall typeclass is
parameterized by a type constructor of arbitrary kind. I cover this in my blog
post here: [http://www.calebh.io/Monads-as-Programmable-
Semicolons/](http://www.calebh.io/Monads-as-Programmable-Semicolons/)

------
state_less
I always thought it was beautiful that Haskell turned type construction into
just another function on a different domain. What do people call this
abstraction?

~~~
kccqzy
Type-level functions are called type families. They are really not just
another function. But if you appreciate that beauty, check out a dependently
types language like Idris where normal functions can be used on the type-level
as well. So you might have a vector type with a length computed by a
complicated expression.

------
mlevental
what is this blog format/static site generator thing?

