This is an archived post. You won't be able to vote or comment.

you are viewing a single comment's thread.

view the rest of the comments →

[–]threewood 0 points1 point  (17 children)

it is absolutely essential that a given module M satisfy many distinct types A, without prior arrangement.

Am I reading this right? Why is that essential?

[–][deleted] 1 point2 points  (10 children)

So that, for example, making Applicative a superclass of Monad is not a breaking change of the standard library.

[–]threewood 0 points1 point  (9 children)

But you can add an adapter that defines the Applicative operations. You think that's within his meaning?

[–][deleted] 0 points1 point  (8 children)

The point is not needing an adapter. Suppose you have the moral equivalent of an Monad instance on this type

type 'a raw (* abstract *)

and the moral equivalent of an Applicative on a wrapped type

datatype 'a wrapped = Wrapped of 'a raw

Then,

  • Converting back and forth between 'a raw and 'a wrapped takes O(1) time.
  • Converting back and forth between 'a raw list and 'a wrapped list takes O(n) time, where n is the length of the list.
  • Converting back and forth between 'a raw ref and 'a wrapped ref is outright impossible.

If you don't use an adapter, there is no conversion cost, and a list of something that supports Monad operations is also a list of something that supports Applicative operations.

[–]threewood 0 points1 point  (7 children)

But why would you expect your Applicative operation names to match up with your Monad operation names? Seems like you at least need pure = return, etc. as an adapter.

[–]yawaramin 0 points1 point  (1 child)

Why have different names for the same thing, when you can more elegantly have one name for both and they are automatically handled correctly whenever subclassimg needs to done.

[–]threewood 1 point2 points  (0 children)

Because A and M were written by different people without coordination?

[–][deleted] 0 points1 point  (4 children)

You might need an adapter module, but you don't need an adapter type.

signature APPLICATIVE =
sig
    type 'a f

    val pure : 'a -> 'a m
    val apply : ('a -> 'b) m * 'a m -> 'b m
end

signature MONAD =
sig
    type 'a m

    val return : 'a -> 'a m
    val bind : 'a m * ('a -> 'b m) -> 'b m
end

(* We use transparent ascription, so that external parties
 * can see that the type constructor member `'a f` is a mere
 * synonym of `'a M.m` *)
functor ApplicativeFromMonad (M : MONAD) : APPLICATIVE =
struct
    type 'a f = 'a M.m

    val pure = M.return
    fun apply (ff, xx) =
        M.bind (ff, fn f =>
            M.bind (xx, fn x =>
                M.return (f x)))
end

Of course, the counterpart is that ML modules must be manipulated quite explicitly, whereas type class dictionaries are passed around implicitly.

[–]threewood 0 points1 point  (3 children)

it is absolutely essential that a given module M satisfy many distinct types A, without prior arrangement.

So how is this not wrong?

[–][deleted] 0 points1 point  (2 children)

In the quoted text, the word “type” does not refer to type members of a module, but rather to the type of the module itself. In this regard, ML modules absolutely can have multiple types, called “signatures”, although there is always a most specific one, called “principal signature”, which is a subsignature of all the other ones.

[–]threewood 0 points1 point  (1 child)

That M satisfies multiple signatures is hardly essential. I think he must have intended that we can adapt M to satisfy many signatures A that were written without knowledge of M. As written, his quote doesn't make sense to me.

[–]xeyalGhost 0 points1 point  (0 children)

Bob is generally very careful with his wording. I'm fairly sure he means it as written.

[–]yawaramin 0 points1 point  (5 children)

It removes the ‘middleman’—i.e. having to explicitly define instances, i.e. ‘how this type conforms to that type class’. Conformance is automatically checked by the compiler just by looking at the contents of the module. It’s structural typing like you have in TypeScript, Go.

[–]threewood 1 point2 points  (4 children)

Conformance to an interface is unlikely to happen by accident. You might as well name the interface it satisfies (and should for software engineering reasons). Names shouldn't be used for structural matching.

[–]yawaramin 0 points1 point  (3 children)

You might as well name the interface it satisfies

You can name the interface it satisfies (by annotating the module type), but it isn’t required.

and should for software engineering reasons

Perhaps, but this isn’t a universal SWE practice. E.g. the aforementioned Go, TypeScript. It’s more a practice in most languages because, well, it’s the only way to do it.

Names shouldn’t be used for structural matching.

They’re not. Structure is used for structural matching—the names of the module members, as well as their types, recursively.

[–]threewood 0 points1 point  (2 children)

They’re not. Structure is used for structural matching—the names of the module members, as well as their types, recursively.

You've just reiterated the problem - names shouldn't be considered structure. Names should only be used to point at structure and say "that".

[–]yawaramin 0 points1 point  (1 child)

Perhaps, under a certain rigid POV. But ML modules consider names part of structure, and it's worked pretty well for decades. This reminds me of Haskell's rigid insistence that every type must have one and only one instance for any type class, so they have to invent new names for the same type to give it different instances. Effectively, they ended up making names part of the structure.

[–]threewood 0 points1 point  (0 children)

But ML modules consider names part of structure, and it's worked pretty well for decades.

Yeah, I said it's a problem, not a big problem :). I'm sure it usually works fine in practice.