~icefox/garnet#70: 
Make modules/interfaces/platypi not terrible to actually use

It's not quite time to tackle this yet, but it's getting close. There's some narsty ergonomic issues with modules as implemented in OCaml, and while in principle I'm okay just sucking it up and living with it, I do think it's probably possible to make life better than it is. There's a few separate ideas I have for this:

  1. The syntax and mechanics of just implementing modules and functors. This is basically summed up here: https://todo.sr.ht/~icefox/garnet/53#event-386319 . I quite like the results I ended up with there, but I'm not ready to set them in stone yet.
  2. Explicit instantiation of types. Essentially in Rust every time you say let x: Vec<i32> = ... in any part of the code you are very literally creating a new instance of Vec and all its methods, specializing them to i32, and then deduplicating them so they all point to the same actual implementation and typecheck to the same result. That's possible because due to orphan rules, any Vec<i32> is always the same as any other Vec<i32>. With modules this is not the case, it's possible to have two different Vec<i32> implementations that are very distinct things, and so traditionally you always have to create each Vec<i32> manually and thread them through your code. And back in my OCaml days, having to make a new copy of Hashtbl.t (int, string) for each file and explicitly write out that creation for each (int, string) pair I wanted to use and choose which one I want to use was really damn confusing and felt like a lot of needless busy-work.
  3. Having to explicitly name modules to use them. This is related to the second, but broader. To use a particular instance of a module it has to be passed as a normal function argument. I believe this can be done with something along the lines of Modular Implicits, which are pretty much exactly what they sound like: implicit function args that are automatically selected from whatever's currently in scope. Rust has made me automatically distrust anything implicit, and the only language that has implicits like this I know of is Scala, which doesn't exactly fill me with enthusiasm either. But, maybe we can make it not suck via careful API design, and maybe we can figure out how to teach others that style of API design. Elixir also has many namespaces like List and Enum that you call explicitly rather than doing trait-like "dispatch based on the type of the value", and while a little verbose it's honestly not terrible.
  4. Coherence. This is Unsolvable In The General Case, but it would be nice to have some conservative rules on how to make it not bite your ass in the practical cases.
  5. Sealing and signature equivalence and all that jazz. I don't think this is actually necessary at all, and seems to involve a lot of tedious annotations and accounting, and so I am going to leave it out entirely if at all possible. I rather suspect I have accidentally duplicated and then solved the problem simply by having both structural and nominal types and explicit operators to wrap/unwrap them, but I don't know for sure. I'm also punting on visibility modifiers like Rust's pub right up until something forces me not to, which helps.
Status
REPORTED
Submitter
~icefox
Assigned to
No-one
Submitted
7 months ago
Updated
7 months ago
Labels
T-THOUGHTS

~icefox 7 months ago

I made this entire issue simply so I had a place to put the following conversation:

icefox
Finally getting around to trying out nicer interface/ML module/platypus syntax. Not final, but I don't hate it.

--- functor returning a struct implementing a particular module
impl fn TryFromFrom(|In| from_impl: From[In]) TryFrom[In] =
    type Self = from_impl.Self
    type Err = Never
    try_from: fn(input: In) Result[Self, Err] =
      Result.Ok(from_impl.from(input))
    end
end

Vi
Btw name suggestion for the impl block: `TryFromFromFrom`
Because its making TryFrom from From
Big improvement
Do users always manually invoke module impls?

icefox
So far yes, but there's vague plans for some kind of modular implicit functionality

Vi
Could be nice to have anonymous module impls in that case, and allow:

impl fn(|In| from_impl From[In]) TryFrom[In] =
    ...
end

callable modules could help with this? 

// ugly syntax just to demo
impl Vec =
    fn call_module(|a|) =
        fn pop(self) a = ... end
    end
    fn new(|a| values: Iterator[a]) Vec[a]
end
Vec.new(1, 2, 3)

icefox:
Anyway this kinda leans hard on the function call inference and coalescing of pure modules... But... The function call inference is there already and I was already leaning hard on purity for (most) module impl's.
You'd have to be able to name Vec[I32] as a type in some random places but I'm 99% sure you already can... :thinking: just feels a little weird. 

Vi
Yeah I think having Vec(u32) = Vec(u32) is p important with modules

...

icefox
Circling back around again, what is the `fn pop(self)` here doing? Listing the functions that are accessible when calling the module implicitly?

Vi
So the idea there is that Vec and Vec[T] are actually completely separate modules, and the functions that depend on T (like pop) live in Vec[T], then we can also include all of Vec inside Vec[T]
Though that was sorta open to interpretation :) There's probably some tweaks that'll make that more ergonomic

~icefox 7 months ago

Soooooo it's fuzzy, but I rather like the idea of not having to do this:

const VecU32 = Vec(|U32|)
let mut x = VecU32.new()
VecU32.extend(x, [1,2,3])

but rather just doing:

let mut x = Vec[U32].new()
Vec.extend(x, [1,2,3])

In principle, it's totally possible to infer a type for Vec.extend() because we already know the type for x, and then it's possible to instantiate an instance of the module Vec with that type. As long as we have only one instance of Vec[U32] (which is the usual case) then it's unambiguous and as long as creating Vec[U32] is pure then there shouldn't be any coherence issues. If I'm understanding correctly. Or, to lean less on sugar and inference, as Vi said you could have Vec be a separate module that takes some type parameter and attempts to dispatch off to another module chosen by modular implicits??? Or include one module inside the other (which is not implemented in Garnet yet and which I haven't had much need for yet, since it smacks of subtyping.)

~plecra is this vaguely accurate as to what you were thinking?

What would that look like with modular implicits, actually? Let's try it out:

const thingy = Vec(|U32|)
-- We need another module `Vec`, separate from `Vec[T]`, that takes an implicit impl of `Vec[T]` and just calls out to it
let mut x = Vec.new(|U32|) -- actually calls `Vec.new(|U32| thingy)`
Vec.extend(x, [1,2,3]) -- actually calls `Vec.new(thingy, x, [1,2,3])`

Hmmm. It works? I think? (I could be totally wrong, it's been a long day.) But it doesn't actually solve the explicit-instantiation problem, and in general feels a bit like taking the long way around.

Register here or Log in to comment, or comment via email.