ilkut kutlar
ilkut kutlar

Reputation: 41

Is there are a way to use OCaml's type system to enforce rules about values?

I am quite new to OCaml and have been exploring the capabilities of the type system. I have two questions.

  1. In this record type, is there a way to use OCaml's type system to enforce the rule that nums1 + nums2 = all_nums?
type foo = {all_nums: int list; nums1: int list; nums2: int list;} ;;

e.g. if nums1 = [1] and nums2 = [2; 3] then all_nums must be [1; 2; 3].

  1. In this record type, is there a way to enforce using the type system that no item in nums1 should also be in nums2, i.e. their intersection should be empty?
type bar = {nums1: int list; nums2: int list;} ;;

e.g. if nums1 = [1], nums2 cannot contain 1 as well.

Thank you in advance.

Upvotes: 3

Views: 485

Answers (1)

ivg
ivg

Reputation: 35210

Yes and no. A type that depends on a runtime value is called a dependent type, and OCaml doesn't support dependent types to the full extent. It is not typical for conventional programming languages to support them since they are making programming quite cumbersome. For example, theoretically, OCaml has enough dependent typing to support your case, except that you won't be able to use list or int, and you will have to use GADT representation for them. And at the end of the day, it would be hardly usable. Because, OCaml type system is still static, so it must check that your program is valid for all possible sets before the program execution. This will limit the set of typable programs a lot.

However, using abstract types coupled with phantom types, it is possible to encode arbitrarily invariants and rely on the type system to preserve it. The trick is to define a small trusted kernel, where the invariant is enforced manually.

Taking your first example,

 module Foo : sig 
   type t 
   val create : int list -> int list -> int list -> (t,error) result
 end = struct 
   type t = {all_nums: int list; nums1: int list; nums2: int list;}
   type error = Broken_invariant
   let create all_nums nums1 nums2 = 
      if invariant_satisfied all_nums nums1 nums 2 
      then Ok {all_nums; nums1; nums2}
      else Error Broken_invariant
 end

Using this sealed representation it is impossible to create outside of the module Foo a value of type Foo.t for which the invariant_satisfied is not true. Therefore, your Foo is the trusted kernel - the only place where you need to check that the invariant is preserved. The type system will take care of the rest.

You can encode much more complex invariants and be more expressive if you will use phantom types, e.g.,

 module Number : sig 
     type 'a t 
     type nat
     type neg
     type any = (nat t, neg t) Either.t


     val zero : nat t
     val one : nat t
     val of_int : int -> any
     val padd : nat t -> nat t -> nat t 
     val gadd : 'a t -> 'b t -> any
 end = struct 
     type 'a t = int 
     type nat 
     type neg
     type any = (nat t, neg t) Either.t

     let zero = 0
     let one = 1
     let of_int x = if x < 0 then Right x else Left x
     let padd x y = x + y (* [see note 2] *)
     let gadd x y = of_int (x + y)
 end

where the Either.t type defined as type ('a,'b) t = Left of 'a | Right of 'b


Notes

Note 1. Your first example could be encoded in such a way that it is not possible to break the invariant, for example, instead of duplicating your data in all_nums, you can represent your types {nums1 : int list; nums2 : int list} and define all_nums as a function, let all_nums = List.append

Note 2. In fact since OCaml, like many other programming languages, is using modular arithmetics, an addition of two positive numbers can lead to a negative number, therefore our example is botched. But for the sake of example, let's ignore this :)

Upvotes: 4

Related Questions