Risto-Stevcev/bastet

Polymorphic variant for ordering

andywhite37 opened this issue · 4 comments

Hi @Risto-Stevcev - first of all, thanks for making this library, it's great to have an un-opinionated base of core functionality like this to build on!

I just had a quick question on the ordering type https://github.com/Risto-Stevcev/bs-abstract/blob/master/src/interfaces/Interface.re#L130 . I was curious why it was implemented as a polymorphic variant, rather than just a normal variant like type ordering = | LT | EQ | GT. I didn't look at all the usages, but I was curious if making it polymorphic enabled some kind of functionality?

@texastoland mentioned the usefulness of the tags being public - maybe that was the reason?

Yeah that's definitely one use, they're just more expressive in general.

Polymorphic variants are type level variants, the constructor is available as a type:

type equal_to = [ `equal_to ]
type greater_than = [ `greater_than ]
type less_than = [ `less_than ]

type ordering = [ equal_to | greater_than | less_than ]

The subtyping is a really nice feature -- polymorphic variant values work anywhere that accepts that variant as a member of the set without any extra wrapping and conversion:

type greater_than_equal = [ greater_than | equal_to ]
type less_than_equal = [ less_than | equal_to ]

let f: greater_than_equal -> unit = fun _ -> ()
let f': ordering -> unit = fun _ -> ()

let x = `greater_than
let y = f x
let z = f' x

You can also have functions take open variants, or any variant as long as it has the given types. Think of > as at least (these variants):

type 'a ordering_a = [> ordering] as 'a

let z: ordering = `equal_to
let g: [> `less_than | `greater_than] -> unit = fun _ -> ()
let w = g x
let w' = g z

You can also have lower bounds on variants, where the type given has to be a subtype of the variant. Here less_than_equal is a subtype of ordering. Think of < as no more than (these variants):

let h: [< ordering] -> unit = fun _ -> ()
let x': less_than_equal = `less_than
let _ = h x'

You can also combine upper and lower bounds:

let g': [< `less_than | `equal_to | `greater_than > `greater_than ] -> string = fun x ->
  match x with
  | `greater_than -> "greater"
  | _ -> "not greater"

let z': less_than_equal = `less_than
let z'': greater_than_equal = `greater_than
let _ = g' z' (* doesnt compile *)
let _ = g' z''

The line with "doesnt compile" won't compile because the function is looking for any variant that contains no more than less_than | equal_to | greater_than but must contain at least greater_than, and the less_than_equal type doesn't have that variant

And you match on poly variants based on their subtypes with the # syntax, which is super useful to avoid iterating over every single possible variant:

let print_lte: less_than_equal -> string = function
| `less_than -> "less than"
| `equal_to -> "equal to"

let print_gte: greater_than_equal -> string = function
| `greater_than -> "greater than"
| `equal_to -> "equal to"

let print_ordering: ordering -> string = function
| #less_than_equal as lte -> print_lte lte
| #greater_than_equal as gte -> print_gte gte

The argument in favor of ordinary variants is that there is not that relaxed restriction between constructor and variant. Constructors aren't first-class and type-level like poly variants, but the upside is that the constructor is tied to a specific variant. It's a tradeoff basically:

type foo_toggle = On | Off
type bar_toggle = On | Off
let ff: foo_toggle -> unit = fun _ -> ()
let foo: bar_toggle = On
let _ = ff foo (* doesnt compile, not a foo_toggle *)

But you can have your cake and eat it too by just wrapping your poly variant structures using either an abstract type deriving functor if you need it to be opaque or a singleton ordinary variant like this:

type toggle = [ `on | `off ]
type foo_toggle = FooToggle of toggle
type bar_toggle = BarToggle of toggle
let ff: foo_toggle -> unit = fun _ -> ()
let foo: bar_toggle = BarToggle `on
let _ = ff foo (* doesnt compile, not a foo_toggle *)

Poly variants are my favorite feature of Ocaml, you can write very expressive type-level code with them, I wouldn't be able to enforce much of any of the CSS and HTML spec from https://github.com/Risto-Stevcev/bs-declaredom at compile time without poly variants for example,

Cool, thanks for the explanation! This all makes sense.