Inductive type in Swift is quite easy, e.g. List, Tree in Swift built with indirect enum, so we talk about co-inductive type in Swift, which is the dual counterpart of inductive type in Swift, which is somewhat impossible (but not really), due to the lackness of lazy evaluation by default.
In Haskell, data type Stream is a co-inductive type, implemented as follows:
GHCi, version 8.10.7: https://www.haskell.org/ghc/ :? for help
Prelude> data Stream a = a :> Stream a
Prelude> :k Stream
Stream :: * -> *
Prelude> makeNums n = n :> makeNums ( n+1 )
Prelude> :t makeNums
makeNums :: Num t => t -> Stream t
Prelude> let nums = makeNums 10
Prelude> :t nums
nums :: Num t => Stream t
Prelude> head (a :> rest) = a
Prelude> head nums
10
Prelude> tail (a :> rest) = rest
The behavior of a stream is that it can generate an element when you request forever. But how a stream can do it.
-
1. A stream has a state. It is
nin the above example. -
2. When the stream generates an element,
nin the above example, it derives a new stream with a new state from itself. They aremakeNums( n+1)and(n+1)accordingly in the above example.
Now, the question is how can we implement it in Swift.
We can mimic the Haskell implementation in Swift by Type Operator Convertions:
data Either a b = Left a | Right b // Either a b = a | b
data Polynomial a b c = Left a b | Right c // Polynomial a b c = a * b | c
then, we can transform those data type into Swift by using tuple or struct for Product and enum case for Sum.
enum Either<A, B> {
case Left(a: A)
case Right(b: B)
}
enum Polynomial <A, B, C> {
case Left(a: A, b: B)
case Right(c: C)
}
Using this convertion, then we have stream implementation in Swift as followings, but we also need indirect enum to help for recursive type operator in Swift for type operator self-reference.
indirect enum Stream <T> {
case x (head: T, tail: Stream<T>)
}
Now we have a Stream type, the problem is how we can instantiate an instance for it. Let us imitate Haskell implementation again as follows:
func makeNums(i : Int) -> Stream<Int> {
return Stream.x(i, tail: makeNums(i+1))
}
Aha, we have it in Swift as well. However when we call makeNums function to create a Stream of Int, the function will run until the stack blows up.
Because Swift is eager evaluation, it will call makeNums in turns when meets, whereas Haskell will call makeNums when needs.
So our direct implementation is not succeeded. Is it impossible to implement a co-inductive type operator in Swift?
The answer is no, we need to do it from ground up, that is a instance of co-inductive type (e.g. stream) has a state, when it generates an element, it derives a new instance (e.g. stream) with a new state. Then it generates, it do it again and again.
In other words, we need to think through it, where we need lazy evaluation and where we do not need. And in Swift, there is a way to make lazy evaluation possible, i.e. @autoclosure, or embeded computation into a function. Then we can do lazy evaluation for the needed part to implement our Stream in Swift as follows:
struct Stream <T> {
let head: T
let tail: () -> Stream<T>
init(h: T, t: @escaping @autoclosure () -> Stream<T>) {head = h; tail = t}}
func makeStream(i: Int) -> Stream<Int> {
return Stream<Int>(h: i, t: makeStream(i: i+1))}
func head<T>(_ s: Stream<T>) -> T { return s.head }
func tail<T>(_ s: Stream<T>) -> Stream<T> { return s.tail() }
let s1 = makeStream(i: 1); print(head(s1))
let s2 = tail(s1); print(head(s2))
The @autoclosure is crucial here, it decorates the parameter type of tail part in the constructor of Stream, that causes the expression makeStream(i: i+1) is not evaluated yet until the tail function is called, which should return a Stream not a computation of Stream.
If you remove @autoclosure decoration there, you need to enclose the makeStream(i: i+1) into curly brace to form a function of () -> Stream. that looks not that naturally as before.
The key point is to identify which part is lazy evalution (computation) required, which is not (eager evaluation (value) required), then use @autoclosure or enclosed function to pass a computation, or plainly pass a value.
Cool, right?