What's the OCaml way to do multiple interfaces?

OMG THIS WORKED!!! :partying_face:

1 Like

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"
2 Likes

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

4 Likes

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. :wink:

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.

1 Like

Indeed, it was the article I had in mind.

I won’t argue against you, and it’s great to see that you change your mind an opinion since you wrote https://linuxfr.org/users/bluestorm/journaux/pourquoi-la-recherche-en-langages-de-programmation#comment-1717176:

Je pense que c’est assez clair que Go 1 ignore complètement les 40 dernières années de recherche en langages de programmation (à l’exception des primitives de concurrence qui ont inspirés ses goroutines). Pour tout le reste, l’absence de generics, de sum types, la présence de nil et de interface{} sont clairement aux antipodes de la plupart des systèmes à la pointe de la recherche (par exemple mezzo, ou même rust) qui sont plutôt du coté fonctionnel avec un typage expressif

With a translation for non french reader:

I think it’s pretty clear that Go 1 completely ignores the last 40 years of research in programming languages (with the exception of the concurrency primitives that inspired its goroutines). For everything else, the absence of generics, sum types, the presence of nil and interface{} are clearly at odds with most of the systems at the forefront of research (for example mezzo, or even rust) which are rather on the functional side with express typing

Don’t forget that at this time I take the defense of Golang programmer. :wink:

Are you confusing me with @gasche? :slight_smile:

I agree about go ignoring decades of PL research anyway, but not because of interfaces; just because of the rest. You could have a great ML with interfaces and sum types and proper support for FP.

3 Likes

I don’t think so. For a very a good reason that at @gasche has a quality that we (you and me) don’t have is diplomacy. For sure, he expressed that Golang is not so much interesting for the kind of research he does, but he was not so rude as you were.

I should add that, from a typing point of view, what you’re promoting is Golang without genericity and without convenience for ergonomy (from the caller side).

I’m genuinely curious, is what’s being discouraged here the use of objects in OCaml in particular or do you also discourage the use of first class modules and other techniques for abstracting over multiple different concrete types?

I’m new to OCaml from languages like Swift or Rust, so not sure if I’m forcing something that isn’t natural to OCaml and regular users of it. I really like something like Rust traits for how it helps write generic code and promote standards and documentation in a codebase, but I’m open to learning a different way of doing things.

I dunno about what others mean to encourage or discourage, but I feel moved to give this perhaps not entirely unsolicited advice.

  1. You can do quite a lot with just ordinary functions and algebraic data types. Don’t reach for any the advanced language features until you’re sure you need them. A lot of the time, you ain’t gonna need them.

  2. The module and functors language can usually get you the other 90% of the way to where you need to go when ordinary functions and algebraic data falls short. You’ll know you need it when you find yourself needing to write functions at the type level rather than value level.

  3. The class and object language should be your last resort. Not only is method dispatch slower than you think (because objects are structurally typed, not nominally typed). You’ll know you need it when you find yourself really needing to use the constraint keyword and the ( :> ) operator a lot. (This is an unnatural concept to most Rust programmers, because it has no subtyping relation between types of the regular kind.)

Hope that helps.

4 Likes

I appreciate the advice! There is much power in YAGNI.

I’m sorry to have diverge from your original question. For sure, you can use object and it will feed your needs:

I want a list of values that have this behavior, I don’t care what their concrete type is

If it’s your need, object is one of a possible solution.

1 Like

Oh dear.

Maybe you were pointing to my comment in this thread (my browser didn’t scroll to it but that would explain the confusion). I forgot about that comment so I basically made the same again right here and now :slight_smile:

5 posts were split to a new topic: On the design of iostream