A pattern for object-like components in functional languages
There’s been a lot of discussion recently about how and when to break up state and how and when to interpose module and data type boundaries in elm on the elm-discuss list. I’ll discuss here something that took me some time to formalize that relates to object thinking in functional languages.
I’ve written before on the topic of isomorphism between object oriented polymorphism and union types in functional languages. One thing I hadn’t realized until I’d written it a couple of times is that there’s a pretty convenient and relatively easy pattern for object-like values in functional languages.
I’ve been writing F# via fable recently, and it gave me an opportunity to fully explore the benefits and drawbacks of polymorphic objects vs plain data+function style polymorphism with boilerplate. In both cases, a certain pattern emerged.
TL;DR, Don’t embed an object reference in your methods.
It’s tempting to write something like this:
module Experiment exposing (..)
type Object =
Object
{ value : Int
, set : Int -> Object
, op : Int -> Object
}
-- Set up a base counter we can attach methods to
dummyCounter v =
Object
{ value = v
, set = (\i -> dummyCounter i)
, op = (\i -> dummyCounter i)
}
wrap : (Object -> Int -> Object) -> Object -> Object
wrap setf opf (Object obj) =
let se i = setf (Object obj) i in
let op i = opf (Object obj) i in
Object
{ obj
| set = (\i -> wrap setf opf (se i))
, op = (\i -> wrap setf opf (op i))
}
initCounter op v =
wrap
(\(Object o) i -> Object { o | value = i })
(\(Object o) i -> Object { o | value = op o.value i })
(dummyCounter v)
set i (Object o) =
o.set i
op i (Object o) =
o.op i
-- c = E.initCounter (+) 0
-- d = E.op 3 c
Or maybe not depending on how you think of OOP. This is a pretty standard way of doing it, with method bound to object. So what’s wrong with this?
There are a lot of things wrong:
- It’s verbose for what it does
- It’s pretty fragile
- It’s expensive. Every update requires at least 2 allocations
- You can’t recursively call methods because you’re stuck with a method view that’s 1 generation behind
- The tragedy of it is that it’s actually not possible to destructure the object in place in most contexts, so you have helper functions anyway
The correct way of defining this object is in a more functional style:
module Experiment exposing (..)
type Object =
Object
{ value : Int
, set : Int -> Object -> Object
, op : Int -> Object -> Object
}
— Set up a base counter we can attach methods to
dummyCounter v =
Object
{ value = v
, set = (\i (Object o) -> Object { o | value = i })
, op = (\i (Object o) -> Object { o | value = i })
}
initCounter op v =
let (Object w) = dummyCounter v in
Object { w | op = (\i (Object o) -> Object { o | value = op o.value i }) }
set i (Object o) = o.set i (Object o)
op i (Object o) = o.op i (Object o)
-- c = E.initCounter (+) 0
-- d = E.op 3 c
The key difference is that the object carries functions but the functions are not bound to state. You always pass in the same object and the methods use but don’t retain the state.
F# does this magic for your with object types, but even then this can be more elegant. The right way to do it is usually to use a functional metaphor. Functions and objects really are harmonious when you get down to it.