What is the gleam way to handle separation of behavior and data? #3242
-
I will preface this by saying that my experience in programming languages is limited to those that give access to some form of interface/typeclass trait, or some form of duck-typing. A pretty frequent issue is how to generalize a behavior to multiple types, so that the rest of the code can concentrate on what the used types can do instead of what they are specifically. I am not sure how this is supposed to be handled in Gleam: It doesn't have interface-like constructs to bound generics, and doesn't support duck-typing. From checking the language, the only way to do this seems to be with generics accompanied with functions for the behavior. As an example of what I mean, I've tried to quickly and roughly re-implement a generic fmap function.
With all this, what is the gleam way?
import gleam/io
import gleam/option.{type Option, Some, None}
import gleam/int as int
pub fn main() {
let l1 = List(1, List(2, List(3, End)))
let l2 = End
let m1 = Just(3)
let m2 = Nothing
let e1 = Left(4)
let e2 = Right(Nil)
let l1_res = fmap2(l1, functorable_list, int.to_string)
let l2_res = fmap2(l2, functorable_list, int.to_string)
let m1_res = fmap2(m1, functorable_maybe, int.to_string)
let m2_res = fmap2(m2, functorable_maybe, int.to_string)
let e1_res = fmap2(e1, functorable_either, int.to_string)
let e2_res = fmap2(e2, functorable_either, int.to_string)
io.debug(l1)
io.debug(l1_res)
io.debug(l2)
io.debug(l2_res)
io.debug(m1)
io.debug(m1_res)
io.debug(m2)
io.debug(m2_res)
io.debug(e1)
io.debug(e1_res)
io.debug(e2)
io.debug(e2_res)
Nil
}
// fmap definition
fn fmap2(
source: a,
functorable: Functorable(a, b, inside_a, inside_b),
applied_func: fn(inside_a) -> inside_b) -> b {
fmap(
source,
functorable.accessor,
functorable.constructor,
functorable.base(),
applied_func
)
}
fn fmap(
source: a,
accessor_func: fn(a) -> Option(#(inside_a, a)),
constructor_func: fn(inside_b, b) -> b,
accumulator: b,
applied_func: fn(inside_a) -> inside_b
) -> b {
let res = accessor_func(source)
case res {
Some(#(val, rest)) -> {
let new_val = applied_func(val)
let new_accumulator = constructor_func(new_val, accumulator)
fmap(rest, accessor_func, constructor_func, new_accumulator, applied_func)
}
None -> accumulator
}
}
// Interface definition
type Functorable(a, b, inside_a, inside_b) {
Functorable (
accessor: fn(a) -> Option(#(inside_a, a)),
constructor: fn(inside_b, b) -> b,
base: fn() -> b
)
}
// Maybe implementation
type Maybe(a) {
Just(a)
Nothing
}
fn access_maybe(val: Maybe(a)) -> Option(#(a, Maybe(a))) {
case val {
Just(v) -> Some(#(v, Nothing))
Nothing -> None
}
}
fn construct_maybe(val, _container) {
Just(val)
}
fn base_maybe() {
Nothing
}
const functorable_maybe = Functorable(access_maybe, construct_maybe, base_maybe)
// List implementation
type List(a) {
List(a, List(a))
End
}
fn access_list(val: List(a)) -> Option(#(a, List(a))) {
case val {
List(v, rest) -> Some(#(v, rest))
End -> None
}
}
fn construct_list(val, container) {
List(val, container)
}
fn base_list() {
End
}
const functorable_list = Functorable(access_list, construct_list, base_list)
// Either implementation
type Either(a, b) {
Left(a)
Right(b)
}
fn access_either(val: Either(a, Nil)) -> Option(#(a, Either(a, Nil))) {
case val {
Left(v) -> Some(#(v, Right(Nil)))
Right(_) -> None
}
}
fn construct_either(val, _container) {
Left(val)
}
fn base_either() {
Right(Nil)
}
const functorable_either = Functorable(access_either, construct_either, base_either) |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
In Gleam we favour solving specific concrete problems and typically avoid seeking as high a level of abstraction as possible. This has benefits in terms of performance and clarity, but in exchange will be less concise than a large codebase using more abstraction. The approach you've taken is fine but it is not what typical Gleam code looks like. Aside: the name "fmap" in Haskell is tech debt! They wanted to call it "map" but it was already taken in the prelude. If you have no such tech debt in your language I say use the proper name of "map" |
Beta Was this translation helpful? Give feedback.
In Gleam we favour solving specific concrete problems and typically avoid seeking as high a level of abstraction as possible. This has benefits in terms of performance and clarity, but in exchange will be less concise than a large codebase using more abstraction. The approach you've taken is fine but it is not what typical Gleam code looks like.
Aside: the name "fmap" in Haskell is tech debt! They wanted to call it "map" but it was already taken in the prelude. If you have no such tech debt in your language I say use the proper name of "map"