I’m trying to reimplement this simple example of how to use traits (Rust) but in OCaml. I thought functors would be the way to go but I’m not getting far going that route. I went asking and hear that functors are actually not very common to write day-to-day (response to my question on X). If that’s the case, how would you all go about reimplementing the example I linked but in OCaml? It’s a pretty common thing in other languages (protocols in Swift, interfaces in C#, etc.), so I figure OCaml must have some way of doing it, but it’s just not clicking for me.
I’m actually trying to extend the example a bit and have OCaml modules implement multiple signatures. Imagine that in the examples I linked that Tweet and NewsArticle implemented not just Summary but also Loggable or something like that. Further, imagine that I had a function that took in modules of just type Loggable and was able to pass the Tweet module and the NewsArticle module to it. Is that possible in OCaml using functors?
Everything you have mentioned is possible once you familiarize yourself with modules and functors. Real World OCaml is a great resource for that. One tip: in OCaml a single module can easily implement multiple interfaces by just implementing the required members of each interface.
That is reassuring! I’ll press on with the learning then. I was concerned for a second I was going the way of the square peg and round hole, but I’ll double down on trying to understand functors and their uses. I’ll give Real World OCaml a look, thank you!
At the risk of starting a flamewar, I’ll say that functors are not a great solution to emulate the dynamic dispatch in other languages.
Functors are nice for some bounded polymorphism patterns such as containers that have requirements on their type parameters (map, set, hashtable, graph, heap, etc). They’re not great when you need to express “I want a list of values that have this behavior, I don’t care what their concrete type is”.
For this, you can pick:
- record of functions. Simple, very clear, but a bit of elbow grease in practice. That’s my go to these days for simple interfaces.
- first class modules. A bit more annoying to consume (you have to move between the expression world and the module world, there’s always a bit of ceremony). Can be nice if you already have the features in existing modules, or if you need a lot of types in your interface.
- objects (interface: “class type”, implementations: either “class” or direct object expressions). Can be trickier at first (and error messages can be confusing) but it’s very powerful. This is the most convenient when you need interfaces that extend other interfaces (eg a type “stream” that extends both input stream and output stream; you can cast one to the other easily and without runtime cost). Eio used to use that and an early post about it had a nice rationale about why objects were used. I think this is more controversial than the other two options but it’s surprisingly powerful if you don’t go overboard with it.
Thanks @c-cube glad to hear another perspective. I struggled a bit trying out functors, and after reading some of Real World OCaml, here’s where I’m currently at reimplementing the toy example I linked. I hope it’s not too much code to continue the discussion. I believe I’m going for the first class modules approach you described, but the code I’m posting does not compile. I commented which line I’m having issues with.
I was expecting (hoping) this to work since my News_article
module implements the required members for the Summary
module, but I can’t pass it to a function that only expects Summary
. The error I get is
This expression has type (module News_article) but an expression was expected of type (module Summary).
module type Summary = sig
type t
val summarize : unit -> string
end
module type Debug = sig
type t
val to_string : unit -> string
end
type news_article = {
headline : string;
location : string;
author : string;
content : string;
}
type tweet = {
username : string;
content : string;
reply : bool;
retweet : bool;
}
module type News_article = sig
include Summary
include Debug with type t := news_article
val value : t
end
let make_news_article content =
let module M = struct
type t = news_article
let value = content
let summarize () =
Printf.sprintf "Headline: %s\nLocation: %s\nAuthor: %s\nContent: %s"
value.headline value.location value.author value.content
let to_string () =
Printf.sprintf "{ headline: %s, location: %s, author: %s, content: %s }"
value.headline value.location value.author value.content
end in
(module M : News_article)
let news_article =
make_news_article
{
headline = "Hello World";
location = "London";
author = "John Doe";
content =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam \
lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam \
viverra nec consectetur ante hendrerit. Donec et mollis dolor.";
}
let content : (module Summary) list = [ news_article ] (* <- I can't pass News_article here even though it implements Summary *)
let process () =
content
|> List.iter (fun (module Item : Summary) ->
print_endline @@ Item.summarize ())
To be extra clear, “I want a list of values that have this behavior, I don’t care what their concrete type is” is exactly what I’m trying to learn how to do. Hoping I can grasp the way to do this, I’ve been impressed with many other aspects of the language.
I recommend first getting the hang of basic functors, before trying to race ahead to first-class modules and other types of abstraction
Remember my hint: each module just needs to implement the required members of each interface, to automatically conform to that interface. So,
module type Summary = sig
type t
val summarize : t -> string (* the t corresponds to Rust's &self *)
end
module type Debug = sig
type t
val to_string : t -> string
end
module News_article = struct
type t = {
headline : string;
location : string;
author : string;
content : string;
}
let summarize { headline; location; author; content } =
Printf.sprintf "Headline: %s
Location: %s
Author: %s
Content: %s" headline location author content
let to_string { headline; location; author; content } =
Printf.sprintf
"{ headline: %s, location: %s, author: %s, content: %s }"
headline location author content
end
Now, the module News_article
automatically conforms to both Summary
and Debug
, thanks to module type inference. You can pass the module into any functor which requires either of those signatures for its parameter, e.g.:
module Short_summary(S : Summary) = struct
let short_summary t = String.sub (S.summarize t) 0 25 ^ "..."
end
module Short_summary_article = Short_summary(News_article)
let short = Short_summary_article.short_summary { headline = "a"; location = "b"; author = "c"; content = "d" }
Great!
Functors will not work at all here because they don’t allow you to have
a list of multiple Summary
with distinct underlying modules.
I was expecting (hoping) this to work since my
News_article
module implements the required members for theSummary
module, but I can’t pass it to a function that only expectsSummary
.
That’s the part about objects I was mentionning, related to
upcasting/mixing multiple interfaces.
Little detail: I like to use all caps names for module types
(distinguishing them from module names). Matter of preference.
This expression has type (module News_article) but an expression was expected of type (module Summary).
let content : (module Summary) list = [ news_article ] (* <- I can't pass News_article here even though it implements Summary *) let process () = content >> List.iter (fun (module Item : Summary) -> print_endline @@ Item.summarize ())
You are really close! What’s missing is an explicit upcast. OCaml never
implicitly casts expressions, each expression has a principal type
(modulo some corner cases).
Try:
let content : (module Summary) list =
[ (module (val news_article : News_article) : Summary) ]
What this does is extract the news_article
as a value (“val” goes from
expressions to the module world) and then re-cast it as a Summary
(whose interface it satisfies).
You could expose a news_article_to_summary : (module News_article) -> (module Summary)
to help readability.
Note that with objects you would also need a cast but it’d look like:
class type summary = object … end
class type news_article = object
inherit summary
…
end
let content : summary list = [ (news_article :> summary) ]
(:>
is the expression-world upcast operator).
Thank you both @yawaramin @c-cube for your feedback. My head is brimming with ideas.
Would you be able to give a brief example of what you mean by record of functions? I’m aware that records can have fields that are functions, but I’m curious to see how that could be used as interfaces.
OMG THIS WORKED!!!
Simple example:
type out_stream = {
output: string -> unit;
flush: unit -> unit;
close: unit -> unit
}
let out_of_buf (buf:Buffer.t) : out_stream = {
output = Buffer.add_string buf;
flush = ignore;
close=ignore
}
let out_of_unix_fd (fd:Unix.file_descr) : out_stream = {
(* note: there should be a loop here actually, to deal with partial writes *)
output = (fun s -> Unix.write fd (Bytes.unsafe_of_string s 0 (String.length s));
close = (fun () -> Unix.close fd;
flush = ignore
}
let write_hello (out:out_stream) =
out.output "hello world"
I find Unison’s new OCaml version-agnostic serialization module a really cool example of this kind of modelling: https://github.com/bcpierce00/unison/blob/master/src/ubase/umarshal.ml
In OOP, you could have classes taking different data through their constructors while implementing the same interface, and I’ve found that partial application enables more or less the same thing in FP.
(* A record taking a function that modifies a string. *)
type t = { f_modify_string : string -> string }
let trim_string str = String.sub str 0 (String.length str)
let trim_record = { f_modify_string = trim_string }
(* Specialisation a function to fit the signature. *)
let concat_strings prefix postfix content = prefix ^ content ^ postfix
let concat_brackets content = concat_strings "(" ")" content
let concat_record = { f_modify_string = concat_brackets }
(* Another example, this time ignoring _ the last string argument, although you probably don't want to do this most of the time. *)
let multiply_chars chr multiply_times _ = String.make multiply_times chr
let multiply_rec = { f_modify_string = multiply_chars 'a' 20 }
Indeed, If you don’t mind static type checking, it’s a solution.
People who write OCaml are generally fine with static type checking :^)
Wait and see. If it seems correct in theory, it’s not as obvious in practice.
As soon as you pack your value in an existential type (aka object) you’re loosing statically any type information you have. And so, you end up with a DSL with only one type. There is a blog post from a SML maintainer, that i can’t find again, where he clearly explains that dynamically typed languages are languages with only one static type.
Was it this one? Dynamic Languages are Static Languages | Existential Type
That’s a great post but, yes, it’s the whole point: you want a single type to stand for multiple concrete types because you are abstracting. There’s no link to dynamic typing, you still get full type safety at the granularity you prefer.
Static typing doesn’t mean static dispatch, thankfully. It’d be very restrictive otherwise.