Destructure anything in rust

art yerkes
3 min readFeb 5, 2023

--

In rust, match expressions are really nice, but because containers don’t pass through the properties of the things they contain (except certain types like enums and slices), it’s sometimes hard to express structure of complex data in match expressions and similar.

Recently some code I wrote early on in learning rust got a sour look, so I decided to exercise my wild haskell side and write a type based destructuring DSL. You can absolutely do this in rust, and it’s amazing… The objects in question are part of a slab allocator, and yet it’s now possible to express matches on them structurally with patterns.

tldr: Haskell inspired destructuring with ad-hoc types.

https://gist.github.com/prozacchiwawa/5cd35ec202ea756b106c8fbe8d28d1a1

Imagine that your code pulls some things out of a serde_json input:

{"texture":"test.png","coords":[3, 6, "-9299344949223"]}

Well, the string might be good enough, but what if you’re often interoperating with yaml and things get wierd?

{"texture": false, "coord": [99, 302, "-9299344949223"]}

So you’ve already got a lot of work to do:

Pick out fields by name (from Map, not doable with match or patterns in rust).

You’ve got to

  • Recognize either bool or string as string
  • Recognize either number or a convertible string for i64

It occurred to me that because traits are very like typeclasses, you can implement a class for a very specific incarnation of some object:

We can start with leaves:

pub enum Str {
Here,
}

...

impl<E> SelectNode<String,E> for Str where E: Default {
fn choose(&self, v: &serde_json::Value) -> Result<String,E> {
match v {
serde_json::Value::String(s) => {
Ok(s.to_string())
}
serde_json::Value::Bool(b) => {
Ok(b.to_string())
}
_ => Err(Default::default())
}
}
}

So calling Str::Here.choose(&json) returns Ok(String) when it matches our wacky definition. That’s neat, but not useful yet.

Consider this:

pub enum Field<T> {
Named(&'static str,T)
}

...

impl<R, T, E> SelectNode<T,E> for Field<R>
where
E: Default,
R: SelectNode<T,E>
{
fn choose(&self, v: &serde_json::Value) -> Result<T, E> {
let Field::Named(name, selector) = self;
if let serde_json::Value::Object(map) = v {
if let Some(found) = map.get(&name.to_string()) {
return selector.choose(found);
}

}

Err(Default::default())
}
}

Working very similarly, Field::Named(“this”,Str).choose(&json) would return Ok(String) on some object containing {“this”:”stringy-thing”} but that’s not the best part.

Now given a Field with some inferred parameter R that also implements SelectNode, we can cause a tree of Field::Named(“this”,Field::Named(“that”,Str)) to do the right thing when choose is called on them. The great thing is this tree can be as deep as we want and rust will infer the right types.

Compounds are easy:

pub enum And<T,U> {
These(T,U)
}

...

impl<R,S,T,U,E> SelectNode<And<T,U>,E> for And<R,S>
where
E: Default,
R: SelectNode<T,E>,
S: SelectNode<U,E>
{
fn choose(&self, v: &serde_json::Value) -> Result<And<T,U>, E> {
let And::These(a,b) = self;
let first = a.choose(v)?;
let second = b.choose(v)?;
Ok(And::These(first,second))
}
}

Since we’re passing through error in the rust way, we can just call choose on our differently typed parameters, each of which implements SelectNode<T,E> for R, SelectNode<U,E> for S. They’re different types (maybe complex types, but we don’t need to know). Since the result is a systematically constructed type, things work.

So how do we extract our json values without writing ad-hoc code? Ad-hoc types:

fn do_match_json(v: &serde_json::value) -> Result<PickedOutData, ()> {
let And::These(
texture,
And::These(
x,
And::These(
y,
z
)
))
= And::These(
Field::Named("texture", Str::Here),
Field::Named(
"coord",
And::These(
Idx::At(0, Integer::Here),
And::These(
Idx::At(1, Integer::Here),
Idx::At(2, Integer::Here)
)
)
)
).choose(v)?;
Ok(PickedOutData { x, y, z, texture })
}

The structures balance (and if they don’t it’ll be a noisy type error). We’ve successfully described what we want to get out in the form of a pattern expression with captures. Depending on how we design these types, we can make the structure look like whatever we want, and it’s “open” as in SOLID, anyone can give us something that implements SelectNode and their expression will work.

I’m blogging this because it extends what I thought was possible in rust. Here’s hoping others will do a haskell in rust like I did.

--

--