On the design of iostream

So, why are you writing code (index (iostream.index)) that is closer to Golang than it is from Rust trait ? (I say this only from a typing point of view).

1 Like

This is kinda getting off-topic. If we want to have a discussion about iostream we should move it to a different thread.

We always like a good debate. But please do not make it about people. @kantian you seem to want to make an interesting point, but I find your messages a bit glib, could you please expand on your point?

It’s almost entirely inspired from Rust’s Read, BufRead, and Write
traits? Please expand on what you mean.

I’d be interested in that thread, but we need somebody with more administrator privs to split the thread. Maybe start a new one?

I was away from keyboard for some time, and I’m sorry if my point was interpreted as attack against person, it was not my intention (it was more an argumentum ad hominem than an argumentum ad persona).

If I had to expand my mind on this question, it is that I don’t like the use of objects when it comes to a unityped API (the characterisation of dynamic languages according to Rober Harper). An object is equivalent to a sum but with an infinity of constructor, and so when you embed your value in this kind of type you don’t know anymore what kind of value you had at first : you have a mammal, sure, but was it a cat, a dog, a human ?

Let’s look at input stream. After following too munch links in my opinion, we finally get the definition. To be an input, it should be possible to read and close your value. Or in another word, you have to satisfy this module interface :

module type Input = sig
  type t
  val read : bytes -> int -> int -> t -> int
  val close : t -> unit
end

And what is an object as defined with this two methods ? It’s a type that is equivalent to the following one:

type 'a meth = (module Input with type t = 'a)
type t = Input : {meth : 'a meth; this : 'a} -> t

Or if we curtify the definition (which leads to an invalid OCaml syntax) we get:

type t = Input : 'a meth -> ('a -> t)

In other words, we get a sum type where each module of the right signature defined a new constructor (each 'a meth module defined a constructor that embed the type 'a in the type t). But when you get a value of type t, you could want to ask the question: was it a string, a byte; a socket ? And you don’t have any way to answer it, be it statically or dynamically (at least with finite sum type you can pattern match, which is a kind of dynamic typing).

But, if you have an API where generic functions over stream as this kind of interface:

val foo : 'a meth -> 'a -> bar

you can still use your string as a stream and never forget that it’s a string, and so use specific method over string on your value.

That’s similar to this kind of code:

module type Ring = sig
   type t
   val one : t
   val add : t -> t -> t
   val neg : t -> t
   val mul : t -> t -> t
end

type 'a ring = (module Ring with type t = 'a)

(* polynomial X^2 + X +1 on an arbitrary ring *)
let poly (type a) ((module R) : a ring) x =
  let ( + ), ( * ) = R.(add, mul) in
  x * x + x + R.one

poly (module Int) 2;;
- : int = 7

poly (module Float) 2.3;;
- : float = 8.59

poly (module Int64) 3L;;
- : int64 = 13L

I didn’t define a sum type in order to write this piece of code:

type t =
  | Int : int -> t
  | Float : float -> t
  | Int64 -> int64 -> t

And, in this case, I will have a problem because I would have to deal with the sum and the product of an int and a float. That’s why there is a problem with object and binary operators as stated in the OCaml manual. It’s because when you consider the category of all algebraic structures that satisfy this interface:

module type Input = sig
  type t
  val read : bytes -> int -> int -> t -> int
  val close : t -> unit
end

there is a terminal object in this category, and the object type is one of the possible definition of the support of this terminal object. But, there is no terminal object in the ring category.

From this point of view, with an object oriented API, and with this object type as the only one entry point, the only way you have to interact with the category is through its terminal object which is, to me, a pain and a loss of information.

But when you get a value of type t, you could want to ask the question: was it a string, a byte; a socket ?

That’s the whole point :-). Information hiding is important (after all this is why OCaml has abstract types). The point is that you establish a clear contract with whatever code you give a iostream, and they can only interact with your concrete type following the contract.

I’m not sure why you don’t like that, and it’s ok; OCaml is a pretty decent language even if you work 100% with concrete types. But for those of us who want some uncoupling, interfaces and objects are just very useful.

2 Likes

I don’t really think that it’s the whole point, and I’m not really sure that you understand what an abstract type is. The cleaner description of abstract type that I read is in this John C. Reynolds article. I don’t mind if you define real number as Cauchy’s sequences or as Dedekind cut, it’s just an implementation detail, and at the end we just get real number. My concern is on those different API:

(* staticly typed API *)
val foo : 'a meth -> 'a -> bar

(* same as before but uncurryfied *)
val foo : 'a meth * 'a -> bar

(* unityped and object API *)
val foo : packed ('a meth * 'a) -> bar

This is bringing us quite far from the original topic, but… isn’t the zero ring the terminal object in the category of rings? Zero ring - Wikipedia

Indeed, my bad. :stuck_out_tongue: But, It didn’t preserve any information of the original ring, as does the terminal object of the Set category (relative to equality of elements) which is just a singleton: type any = Any : 'a -> any. Type that it’s not really possible to distinguish from unit in pure OCaml.

Sorry but it’s difficult to understand from a bunch of type and value definitions exactly what your concrete issue is. What is the actual problem, in practice, with the usage of the iostream API? Can you show with a concrete example?

If you can’t explain it to a six-year-old, you don’t understand it yourself .

-Albert Einstein

2 Likes

Sorry but your argument is not coherent. I am perfectly willing to accept that iostream’s API has a non-optimal design, but I haven’t seen anything convincing regarding that. All you did was write a bunch of definitions, make some claims (at least one of which turned out to be incorrect), and doubted the understanding or intellectual level of anyone who disagreed with you. This is not the kind of discussion we should be having in this forum.

4 Likes

Where is the incoherence ?

One thing that is not clear to me here, in the context of an IO module, is: where do you keep the internal state (say the current position in your stream) ? It cannot be in 'a (since, as you say, 'a could be string so that “you can use it as a string”). So it has to be in meth somewhere. But then how can you prevent a programmer from calling foo on a string meth and an unrelated string ? With the packed version, the source ('a), the methods and whatever state is needed are all bundled together and can’t be unpacked by client code.

But I don’t think both designs are contradictory. You could very well imagine:

type 'a io = { source : 'a;
               (* 'a is "packed" in all those "methods" *)
               read : unit -> char;
               write : char -> unit;
               close : unit -> unit;
              }
let read s = s.read ()
let write s c = s.write c
let close s = s.close ()
let source s = s.source

let of_string s =
   (* packing time *)
    let pos = ref 0 in
    let read () = let c = s.[!pos] in incr pos; c in
    let close () = () in 
    let write _ = failwith "unsupported" in
    { source = s; read; write; close }

and in the mli:

type 'a io

val read : 'a io -> char
val write : 'a io -> char -> unit
val close : 'a io -> unit
val source : 'a io -> 'a
val of_string : string -> 'a io

of course this is a minimal naive design (you may want to also expose seek and so on). And you could also make this safer by using ('a, 'cap) io where 'cap is a polymorphic variant based phantom type to restrict capabilities (i.e. val write : ('a, [> `write]) io -> char -> unit). And also caveat, I have only thought of read/write/close when writing this, I don’t know whether this designs fits all the operations one could want on an IO library.

I don’t really understand your point, but I agree with yawaramin that you were pretty rude calling other people stupid.

5 Likes

I agree that my answer was to @yawaramin was rude, but I felt insulted and I don’t see why I should have stay quiet. But, I don’t see where I said that other people are stupid (it’s not my opinion).

Sometimes one’s tone, especially when the only medium is text, comes across in a way that one didn’t intend (remembering one such situation where I did something similar unintentionally right now) and I think we can all accept you didn’t intend it that way. It’s just the following words sounded like they were doubting your interlocutor’s understanding (and perhaps intelligence).

The recipient probably felt insulted too, the same way the Einstein quote (that one doesn’t understand something well if they can’t explain it simply) made you feel insulted.

Ok I see. But I said this not because I doubt of my interlocutor’s intelligence but because the problem I have with object oriented API has nothing to do whit abstract type. So I didn’t understand why @c-cbue was talking of abstract type, since it’s not the problem.

Suppose you have this type:

type number =
  | Int of int
  | Float of int

Then you have a value v of type number. You could ask this question: is it an int or a float? You can pattern match on your value and have an answer to your question.

Now suppose you have a stream object. You could ask this question: is it a string, a byte or a socket? And now, you don’t have any way to answer your question. :wink:

As an example, do you mean that if you’re trying to copy data from one channel to another, you would like to be able to use something like copyfile on the underlying fds in the case where the 2 channels are backed by files?

I was talking about abstract types as an example of information hiding that is enabled by OCaml.

Now suppose you have a stream object. You could ask this question: is it a string, a byte or a socket?

And the idea is that it’s none of your business and you shouldn’t need to ask. You are only given a clear, limited set of operations to interact with this type. The actual backing type of the iostream might not even be defined yet (in terms of definition order).

3 Likes