Higher-Order-Types and You
alt. title: Forcefully Haskell-fying Rust, for Fun and Profit
If you're as much of a rust fanatic as I am you've probably realized that generics are really cool. In my opinion, rust's composition-based parametric polymorphism is one of the best things its inherited from its functional roots. Today I want to talk about what it left out, and hopefully expand how you think about type systems.
Proper Types
When we talk about types, we often mean something like this:
struct MyData {
name: String,
val: i32
}
This is a simple collection of data. The properties of this type are completely static— name
will always be a String
and val
will always be an i32
. We call these proper types. They are in a fixed and usable state; in other words, we know everything we can know about them.
Types.. kinda
Lets generalize this type to hold any value we could want:
struct MyData<T> {
name: String,
val: T
}
Now this type can composed of any other type, and the “type” T
is actually just a placeholder for what we could later place inside this.
So.. this clearly doesn't fit the definition of a proper type that we previously defined, so what is it? Lets try analyzing what the compiler does with this type under the hood. Lets take the following code:
let data_a = MyData::new("asdf".into(), 12);
let data_b = MyData::new("asdf".into(), true);
And try searching for a MyData
symbol:
nm target/debug/higher_orders_types | grep MyData
And we should see some output like this:
_ZN4core3ptr59drop_in_place$LT$higher_orders_types..MyData$LT$i32$GT$$GT$17h08f26c464aa8b7e0E
_ZN4core3ptr60drop_in_place$LT$higher_orders_types..MyData$LT$bool$GT$$GT$17hf107324ff7a39b31E
Bingo!
These lines say MyData$LT$i32
and MyData$LT$bool
, so we can see that rust will actually generate concrete types for whatever gets used inside the generic! [^1]
Lets generalize this idea back into a type theory context: we can say that MyData
is a special kind of type, that takes a proper type, and returns a proper type.
Improper Types
Just with higher-order-functions, we can see that this type is a level above concrete types. These are called kinds. A kind is a type constructor that takes a type, and produces a new type.
So now that we have a new concept to play with, what can we do with it? Well, lets see what Haskell does with it:
class Functor f where
fmap :: (a -> b) -> f a -> f b
If you don't know any haskell, this likely looks very arcane to you. Lets write it in the style of rust:
trait Functor {
fn fmap<A, B>(
f: impl Fn(A) -> B,
Self<A>
) -> Self<B>;
}
So fmap
takes a function of A -> B
and a Self<A>
, maps said function on the inner value, and returns a Self<B>
. If you try to compile this, you'll get an error akin to “Self takes 0 parameters.” Rust doesn't have a notion of kinds, only parametric polymorphism, so we cant refer to a kind as something on its own. So making this trait will be a little messier, but we can still do it:
trait Functor<A, B>: Sized {
//type Ma; will be Self
type Mb;
fn fmap(f: impl Fn(A) -> B, rhs: Self) -> Self::Mb;
}
We can't guarantee that Mb
will be Self<B>
[^2], but we can at least now use this trait on Option
.
impl<A, B> Functor<A, B> for Option<A> {
type Mb = Option<B>;
fn fmap(f: impl Fn(A) -> B, rhs: Self) -> Self::Mb {
match rhs {
Some(v) => Some(f(v)),
None => None,
}
}
}
Actually, while we're rusti-fying this, lets turn it into a method.
fn fmap(self, f: impl Fn(A) -> B) -> Self::Mb {
match self {
Some(v) => Some(f(v)),
None => None,
}
}
You've most definitely realized at this point that this is identical to a method that already exists, map. If you squint a little, you'll even notice that lots of rust methods come from haskell typeclasses!
Since haskell supports kinds[^3], its able to soundly model these sorts of actions. Its so capable that haskell actually uses a monad to introduce side effects into a purely functional language! Its monads all the way down.
Footnotes
- in this case you can see that rust is actually generating the
drop_in_place
function, so that it can call any destructors when these variables go out of scope - there are actually a lot of ways you could go about solving this, decided to go with a relatively naive/straightforward path
- its probably more correct to say “haskell supports curried type constructors”
- i lightly refered to a type theory page (mostly just to make sure i was using the right words) on wikipedia when i was writing this, check that out for more fp mumbo-jumbo!
maybe sometime ill write more about monads for real, but thats a whole thing that i didnt want to get into. the point of this article was to bridge the gap and expose some of rusts fp roots, and why i think its so cool. thnx for reading! <3