I was recently working with some GADT code and thought of the deepCopy
feature from this thread. Here’s one way someone could make function that deeply copies any sized array:
// Use Peano natural numbers to track array depth.
type rec nat<_, _> = Z: nat<'z, 'z> | S(nat<'a, 'z>): nat<array<'a>, 'z>
let rec copy:
type a z. (array<a>, ~depth: nat<a, z>) => array<a> =
(a, ~depth) =>
switch depth {
| Z => Js.Array2.copy(a)
| S(depth) => Js.Array2.map(a, a => copy(a, ~depth))
}
The nat
type is used to track how deep the array is. For example, S(S(Z))
type-checks with array<array<'a>>
. We can use the function like this:
copy([[1], [2]], ~depth=S(Z))
copy([[[1], [2]], [[3], [4]]], ~depth=S(S(Z)))
Trying to copy deeper than the actual array will produce a type-error:
copy([1], ~depth=S(S(Z)))
// This has type: nat<array<'a>, 'b>
// Somewhere wanted: nat<int, 'c>
//
// The incompatible parts:
// array<'a> vs int
However, it won’t stop you from copying less than the maximum depth, so shallow copies are still possible:
copy([[1], [2]], ~depth=Z) // Only copies the first level
It could be possible to enforce the exact depth, but that would require adding more types. array<'a>
unifies with array<array<'a>>
, so you would need to get rid of the polymorphic 'a
. This probably means writing additional GADT constructors for every possible type, e.g. int
, string
, myCustomRecord
, etc. At that point, I think we’ve hit diminishing returns on how useful the function is.
In almost any practical setting, I think I would still prefer to write deepCopy2
, deepCopy3
, etc. rather than copy(~depth=S(Z))
or copy(~depth=S(S(Z))
, but I also imagine that the GADT version could still be useful in some cases.