Reputation: 41
I am quite new to OCaml and have been exploring the capabilities of the type system. I have two questions.
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].
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
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
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