简体   繁体   中英

how to limit a parameter to only one variant of a variant type

Suppose I have two types, Vector2D and Vector3D and they are tagged (that's the correct term, right?), and I want to write a function that operates ONLY on Vector2D (or is vector(two) more correct here?), like this:

type two;
type three;
type vector('a) = 
  | Vector2D(float, float): vector(two)
  | Vector3D(float, float, float): vector(three);

let first = (a: vector(two)) => {
  switch(a) {
    | (x, y) => x +. y
  }
}

let second = (Vector2D(x, y)) => {
  x +. y
}

let third = ((x, y): vector(two)) => {
  x +.y
}

The first and second functions prohibit passing a Vector3D , just as I want but they raise a warning "This pattern matching is not exhaustive".

For first I'd like to know why this is not exhaustive, have I not limited the possible options to Vector2D here? For second I guess the reason is the same as for first , but how would it even be possible to solve the issue with this syntax?

As for third , this one does not compile because "This pattern matches ('a, 'b) but vector(two) was expected". Why does the compiler expect any tuple here? Is it not possible to use destructuring in function parameters?

EDIT:
Turns out there's an even simpler problem to demonstrate what I want

type state = Solid | Liquid
let splash = (object) => {
  switch(object) {
    | Liquid => "splashing sounds. I guess."
    | Solid => "" // this should not even be possible in the context of this function, and I want to compiler to enforce this
}

Concerning the GADT part, the issue here is that you are using bucklescript and its ancient 4.02.3 compiler version. For instance, the following code works perfectly fine in OCaml ≥ 4.03:

type two = Two ; 
type three = Three ;
/* Never define abstract types when you want to use them as type level tags,
  your code will only works inside the module defining them, and then
  fail due to injectivity problems.
*/

type vector('a) = 
  | Vector2D(float, float): vector(two)
  | Vector3D(float, float, float): vector(three);

let sum_2 = (Vector2D(x,y)) => x +. y
let sum_3 = (Vector3D(x,y,z)) => x +. y +. z 
let sum_any = (type a, x: vector(a) ) => switch (x) {
  | Vector2D(x,y) => x +. y;
  | Vector3D(x,y,z) => x +. y +. z
}

But it will fail on 4.02.3 with an exhaustiveness warning(which should be treated as an error), because exhaustiveness check for GADTs with refutation clauses was only added in 4.03.

You can accomplish what you want using polymorphic variants :

type vector = [
  | `Vector2D(float, float)
  | `Vector3D(float, float, float)
];

let first = (a: [`Vector2D(float, float)]) => {
  switch(a) {
    | `Vector2D(x, y) => x +. y
  }
}

let second = (`Vector2D(x, y)) => {
  x +. y
}

This will give you a type error if you try to pass a `Vector3D to either function:

               ^^^^^^^^^^^^^^^^^^^^^
Error: This expression has type [> `Vector3d(float, float, float) ]
       but an expression was expected of type [< `Vector2D(float, float) ]
       The second variant type does not allow tag(s) `Vector3d

Note first that Vector2D and Vector3D in your example are not types. They are constructors which construct values of the vector('a) type. They are sometimes also called "tags", as you rightly point out, and the type is sometimes called a "tagged union" instead of "variant" because values contain a tag that specifies which constructor was used to construct it. The tag is what's checked when you pattern match using a constructor pattern.

What you're using in your example is not an ordinary variant, but a Generalized Algebraic Data Type (GADT) (which might have been more aptly named "Generalized Variant" in OCaml/Reason), and the reason it does not work is that, while a GADT allows you to assign a specific type to a constructor, it does not work the other way around. You can have several constructors that specify vector(two) for example. (Edit: This seems to be wrong, see @octachron's answer)

Neither the first or third function in your example compiles, The compiler expects a tuple because you use a tuple pattern instead of a constructor pattern. (x, y) is not the same as Vector2D(x, y) . If it was, you wouldn't be able to distinguish Vector2D(float, float) from Line(point, point) , for example, which contains data of an entirely different type.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM