I was watching this video and learned about GADTs in Scala.
I was able to represent the below state machine
Here’s my code
type Idle
type Moving
enum Direction:
case East, West, North, South
import Direction.*
enum Command[From, To]:
case Turn(direction: Direction) extends Command[Idle, Idle]
case Start extends Command[Idle, Moving]
case Stop extends Command[Moving, Idle]
case Chain[A, B, C](command1: Command[A, B], command2: Command[B, C])
extends Command[A, C]
import Command.*
extension [A, B, C](command1: Command[A, B])
infix def ~>(command2: Command[B, C]): Command[A, C] =
Chain(command1, command2)
Now I am looking to change one detail. I want to give the ability to Turn when Idle and also when Moving. And after Turn Command, I want the machine to retain whatever state it was in. For example, if a machine was moving and it takes a turn, I want the state machine to retain that information.
For instance Start ~> Turn(West) should be Command[Idle,Moving] & Start ~> Stop ~> Turn(West) should be Command[Idle,Idle]
I solved it by adding a type parameter to Turn command
Related
So I'm trying to write a dominoes game server, and I'm writing my core types, tiles and sets of dominoes and it occurs to me that including type information for the tile pips would allow me to write a much simpler function that creates chains of dominoes, I've started and left this project incomplete several times due to my being unable to figure this out, hoping someone has a simple type safe tile representation that leads to a simple domino chain function. the reason being is my current mental model is a board in a dominoes games is just an initial tile and 1-3 chains of dominoes each beginning and matching the pips on the initial tile.
Thanks so much in advance, and apologies for any imperfections in my question.
sealed case class DoubleSix[L >: Nat, R <: Nat](lPips: Int, rPips: Int) extends Tile[L, R]
object DoubleSixSet {
val zeroZero: DoubleSix[_0, _0] = DoubleSix(0, 0)
}
an older attempt at the type safe chaining function.
trait DominoChain[End] {
// val hasSpinner: Boolean
}
case object End extends DominoChain[Nothing] {
// val hasSpinner = false
}
case class Chain[D0, D1, X](head: Domino[D0, D1], tail: DominoChain[Domino[D1, X]]) extends DominoChain[Domino[D0, D1]] {
def hasSpinner =
if (head.isDouble || rest.hasSpinner) true
else false
}
As you probably noticed expressing dominoes in types is easy:
sealed trait One
sealed trait Two
sealed trait Three
sealed trait Four
sealed trait Five
sealed trait Six
sealed trait Domino[A, B] extends Product with Serializable
object Domino {
case object OneOne[One, One]
case object OneTwo[One, Two]
... // the other types of dominoes
}
If you want to have a linear chain it is also easy:
sealed trait Chain[A, B] extends Product with Serializable
object Chain {
case class One[A, B](domino: Domino[A, B]) extends Chain[A, B]
case class Prepend[A, B, C](head: Domino[A, B], tail: Chain[B, C]) extends Chain[A, C]
}
Things gets tricky if this is not linear though. You might want to make a turn. There is more than one way of doing this:
xxyy
yy
xx
xx
yy
xx
y
y
y
y
xx
and each of them would have to be expressed as a separate case. If you would like to avoid things like:
f <- f tile would have to be over or under bb tile
aabbc f
e c
edd
you would have to somehow detect such case and prevent it. You have 2 options here:
don't express it in type, express it as value and use some smart constructor which would calculate if your move is valid and return chain with added tile or error
express each turn as a different type, express it in type level and require some evidence in order to create a tile. This should be possible but much much harder, and it would require you to know the exact type in compile time (so adding tile dynamically on demand could be harder as you would have to have that evidence prepared upfront for each move)
But in domino besides turns we can also have branches:
aab
bdd
cc
If you wanted to express it in type, now you have a two heads you can prepend to (and one tail you can append to). And during the game you could have more of them, so you would have to somehow express both: how many branches you have but also to which one do you want to add a new tile. Still possible, but but complicates your code even further.
You could e.g. express heads with some sort of HList (if you are using shapeless) and use that representation to provide implicit telling you which element of HList you want to modify.
However at this point you have very little benefits of type-level programming: you have to know you types ahead of time, you would have difficulties adding new tiles dynamically, you would have to persist state in such a way you would be able to retrieve the exact type so that type-level evidence would work...
Because of that I suggest an approach which is still type-safe while much easier to approach: just use smart constructors:
type Position = UUID
sealed trait Chain extends Product with Serializable
object Chain {
// prevent user from accessing constructors and copy directly
sealed abstract case class One private (
domino: Domino,
position: Position
) extends Chain
sealed abstract case class PrependLine private (
domino: Domino,
position: Position,
chain: Chain
)
sealed abstract case class Branch private (
chain1: Chain,
chain2: Chain
)
def start(domino: Domino): Chain
// check if you can add domino at this position, recursively rewrite tree
// if needed to add it at the right branch or maybe even create a new branch
def prepend(domino: Domino, to: Chain, at: Position): Either[Error, Chain]
}
This will still make it impossible to create an "invalid" domino chain. At the same time it will be much easier to add new rules, expand functionalities and persist state between requests (you mentioned that you want to build a server).
I want to define a tree datastructure that has a label for its edges, here is my initial thought:
sealed trait Tree[+N,+E]
case class Branch[N,E](value:N, children:Map[E, Tree[N,E]]) extends Tree[N,E]
case class Leaf[N](value:N) extends Tree[N, Unit]
As you may guess, if I make a tree instance like following, the type of the instance would be Tree[Int, Any]. I also don't want to have a contravariant type for E.
val tree =
Branch(0,
Map(
"a" -> Leaf(1),
"b" -> Branch(2,
Map(
"c" -> Leaf(4))))
)
I want to know what would be a better way to implement this Tre. I've heard of Fix type classes and recursion schemes but I don't know if they can be useful here.
Have Leaf extend Tree[N, Nothing], not Tree[N, Unit]. Nothing is a bottom type, so that means Leaf[N] will be a subtype of Tree[N, E] for any E since you have it covariant.
case class Leaf[N](value:N) extends Tree[N, Nothing]
With that change, your example tree definition works just fine.
Fixpoint types are more complicated but not always necessary. But you could certainly modify this to make use of them. If you do want to go down that road, it might be better to ask it as another more specific question.
Consider the two codes below. They accomplish the same goal : only such A[T]-s can be stored in the Container where T extends C
However they use two different approaches to achieve this goal :
1) existentials
2) covariance
I prefer the first solution because then A remains simpler. Is there any reason why I ever would want to use the second solution (covariance) ?
My problem with the second solution is that it is not natural in the sense that it should not be A-s responsibility to describe what I can store in a Container and what not, that should be the Container's responsibility. The second solution is also more complicated once I want to start to operate on A and then I have to deal with all the stuff that comes with covariance.
What benefit would I get by using the second (more complicated, less natural) solution ?
object Existentials extends App {
class A[T](var t:T)
class C
class C1 extends C
class C2 extends C
class Z
class Container[T]{
var t:T = _
}
val c=new Container[A[_<:C]]()
c.t=new A(new C)
// c.t=new Z // not compile
val r: A[_ <: C] = c.t
println(r)
}
object Cov extends App{
class A[+T](val t:T)
class C
class C1 extends C
class C2 extends C
class Z
class Container[T]{
var t:T = _
}
val c: Container[A[C]] =new Container[A[C]]()
c.t=new A(new C)
//c.t=new A(new Z) // not compile
val r: A[C] = c.t
println(r)
}
EDIT (in response to Alexey's answer):
Commenting on :
"My problem with the second solution is that it is not natural in the sense that it should not be A-s responsibility to describe what I can store in a Container and what not, that should be the Container's responsibility."
If I have class A[T](var t:T) that means that I can store only A[T]-s and not ( A[S] where S<:T ) in a container, in any container.
However if I have class A[+T](var t:T) then I can store A[S] where S<:T as well in any container.
So when declaring A either to be invariant or covariant I decide what type of A[S] can be stored in a container (as shown above), this decision takes place at the declaration of A.
However , I think, this decision should take place, instead, at the declaration of the container because it is container specific what will be allowed to go into that container, only A[T]-s or also A[S] where S<:T-s.
In other words, changing the variance in A[T] has effects globally, while changing the type parameter of a container from A[T] to A[_<:S] has a well defined local effect on the container itself. So the principle of "changes should have local effects" here favors the existential solution as well.
In the first case A is simpler, but in the second case its clients are. Since there is normally more than one place where you use A, this is often a worthwhile tradeoff. Your own code demonstrates it: when you need to write A[_ <: C] in the first case (in two places), you can just use A[C] in the second one.
In addition, in the first case you can write just A[C] where A[_ <: C] is really desired. Let's say you have a method
def foo(x: A[C]): C = x.t
Now you can't call foo(y) with y: A[C1] even though it would make sense: y.t does have type C.
When this happens in your code, it can be fixed, but what about third-party?
Of course, this applies to the standard library types as well: if types like Maybe and List weren't covariant, either signatures for all methods taking/returning them would have to be more complex or many programs which are currently valid and make perfect sense would break.
it should not be A-s responsibility to describe what I can store in a Container and what not, that should be the Container's responsibility.
Variance isn't about what you can store in a container; it is about when A[B] is a subtype of A[C]. This argument is a bit like saying that you shouldn't have extends at all: otherwise class Apple extends Fruit allows you to store an Apple in Container[Fruit], and deciding that is Container's responsibility.
Consider we have:
abstract class FlyingObject;
case class Rocket(name: String) extends FlyingObject;
what is difference between those two function declarations:
def launch[T <: FlyingObject](fo: T)
and
def launch(fo: FlyingObject)
Great would be some examples when to use which type of declaration...
[UPDATE]
Another great example and explanation can be found there. It's another example of when you should use upper bound instead of just derived class as parameter.
It might be useful to have a T which is more specific than FlyingObject. Perhaps imagine you have a method
def modifyName(fo: FlyingObject, newName: String): FlyingObject = fo.copy(name=newName)
Which returns a copy of the FlyingObject with a modified name. That makes this code not typecheck:
val newRocket: Rocket = modifyName(oldRocket, "new name")
Since modifyName returns a FlyingObject not a Rocket. instead:
def modifyName[T <: FlyingObject](fo: T, newName: String): T = fo.copy(name=newName)
Will return a Rocket when Rocket is what is passed in.
In addition to #stew answer, an upper bound could be useful when using typeclasses. For instance,
suppose you want a method that take two flying objects as well as a collider object defining how to manage collision with other objects. Of course, an asteroid-asteroid collision is not the same as a spaceship-asteroid collision (classical textbook example).
You could write such method as:
def collide[A <: FlyingObject, B <: FlyingObject]
( a: A, b: B )( implicit collider: Collider[A,B] ) = collider.apply(a,b)
Then the compiler will provide a correct Collider for you. If instead you wrote:
def collide( a: FlyingObject, b: FlyingObject ) = a.collide(b)
You will have to rely on Obect-Oriented feature to manage the collision which will be really difficult to write and to maintain (double dispatch issue).
I understand covariance and contravariance in scala. Covariance has many applications in the real world, but I can not think of any for contravariance applications, except the same old examples for Functions.
Can someone shed some light on real world examples of contravariance use?
In my opinion, the two most simple examples after Function are ordering and equality. However, the first is not contra-variant in Scala's standard library, and the second doesn't even exist in it. So, I'm going to use Scalaz equivalents: Order and Equal.
Next, I need some class hierarchy, preferably one which is familiar and, of course, it both concepts above must make sense for it. If Scala had a Number superclass of all numeric types, that would have been perfect. Unfortunately, it has no such thing.
So I'm going to try to make the examples with collections. To make it simple, let's just consider Seq[Int] and List[Int]. It should be clear that List[Int] is a subtype of Seq[Int], ie, List[Int] <: Seq[Int].
So, what can we do with it? First, let's write something that compares two lists:
def smaller(a: List[Int], b: List[Int])(implicit ord: Order[List[Int]]) =
if (ord.order(a,b) == LT) a else b
Now I'm going to write an implicit Order for Seq[Int]:
implicit val seqOrder = new Order[Seq[Int]] {
def order(a: Seq[Int], b: Seq[Int]) =
if (a.size < b.size) LT
else if (b.size < a.size) GT
else EQ
}
With these definitions, I can now do something like this:
scala> smaller(List(1), List(1, 2, 3))
res0: List[Int] = List(1)
Note that I'm asking for an Order[List[Int]], but I'm passing a Order[Seq[Int]]. This means that Order[Seq[Int]] <: Order[List[Int]]. Given that Seq[Int] >: List[Int], this is only possible because of contra-variance.
The next question is: does it make any sense?
Let's consider smaller again. I want to compare two lists of integers. Naturally, anything that compares two lists is acceptable, but what's the logic of something that compares two Seq[Int] being acceptable?
Note in the definition of seqOrder how the things being compared becomes parameters to it. Obviously, a List[Int] can be a parameter to something expecting a Seq[Int]. From that follows that a something that compares Seq[Int] is acceptable in place of something that compares List[Int]: they both can be used with the same parameters.
What about the reverse? Let's say I had a method that only compared :: (list's cons), which, together with Nil, is a subtype of List. I obviously could not use this, because smaller might well receive a Nil to compare. It follows that an Order[::[Int]] cannot be used instead of Order[List[Int]].
Let's proceed to equality, and write a method for it:
def equalLists(a: List[Int], b: List[Int])(implicit eq: Equal[List[Int]]) = eq.equal(a, b)
Because Order extends Equal, I can use it with the same implicit above:
scala> equalLists(List(4, 5, 6), List(1, 2, 3)) // we are comparing lengths!
res3: Boolean = true
The logic here is the same one. Anything that can tell whether two Seq[Int] are the same can, obviously, also tell whether two List[Int] are the same. From that, it follows that Equal[Seq[Int]] <: Equal[List[Int]], which is true because Equal is contra-variant.
This example is from the last project I was working on. Say you have a type-class PrettyPrinter[A] that provides logic for pretty-printing objects of type A. Now if B >: A (i.e. if B is superclass of A) and you know how to pretty-print B (i.e. have an instance of PrettyPrinter[B] available) then you can use the same logic to pretty-print A. In other words, B >: A implies PrettyPrinter[B] <: PrettyPrinter[A]. So you can declare PrettyPrinter[A] contravariant on A.
scala> trait Animal
defined trait Animal
scala> case class Dog(name: String) extends Animal
defined class Dog
scala> trait PrettyPrinter[-A] {
| def pprint(a: A): String
| }
defined trait PrettyPrinter
scala> def pprint[A](a: A)(implicit p: PrettyPrinter[A]) = p.pprint(a)
pprint: [A](a: A)(implicit p: PrettyPrinter[A])String
scala> implicit object AnimalPrettyPrinter extends PrettyPrinter[Animal] {
| def pprint(a: Animal) = "[Animal : %s]" format (a)
| }
defined module AnimalPrettyPrinter
scala> pprint(Dog("Tom"))
res159: String = [Animal : Dog(Tom)]
Some other examples would be Ordering type-class from Scala standard library, Equal, Show (isomorphic to PrettyPrinter above), Resource type-classes from Scalaz etc.
Edit:
As Daniel pointed out, Scala's Ordering isn't contravariant. (I really don't know why.) You may instead consider scalaz.Order which is intended for the same purpose as scala.Ordering but is contravariant on its type parameter.
Addendum:
Supertype-subtype relationship is but one type of relationship that can exist between two types. There can be many such relationships possible. Let's consider two types A and B related with function f: B => A (i.e. an arbitrary relation). Data-type F[_] is said to be a contravariant functor if you can define an operation contramap for it that can lift a function of type B => A to F[A => B].
The following laws need to be satisfied:
x.contramap(identity) == x
x.contramap(f).contramap(g) == x.contramap(f compose g)
All of the data types discussed above (Show, Equal etc.) are contravariant functors. This property lets us do useful things such as the one illustrated below:
Suppose you have a class Candidate defined as:
case class Candidate(name: String, age: Int)
You need an Order[Candidate] which orders candidates by their age. Now you know that there is an Order[Int] instance available. You can obtain an Order[Candidate] instance from that with the contramap operation:
val byAgeOrder: Order[Candidate] =
implicitly[Order[Int]] contramap ((_: Candidate).age)
An example based on a real-world event-driven software system. Such a system is based on broad categories of events, like events related to the functioning of the system (system events), events generated by user actions (user events) and so on.
A possible event hierarchy:
trait Event
trait UserEvent extends Event
trait SystemEvent extends Event
trait ApplicationEvent extends SystemEvent
trait ErrorEvent extends ApplicationEvent
Now the programmers working on the event-driven system need to find a way to register/process the events generated in the system. They will create a trait, Sink, that is used to mark components in need to be notified when an event has been fired.
trait Sink[-In] {
def notify(o: In)
}
As a consequence of marking the type parameter with the - symbol, the Sink type became contravariant.
A possible way to notify interested parties that an event happened is to write a method and to pass it the corresponding event. This method will hypothetically do some processing and then it will take care of notifying the event sink:
def appEventFired(e: ApplicationEvent, s: Sink[ApplicationEvent]): Unit = {
// do some processing related to the event
// notify the event sink
s.notify(e)
}
def errorEventFired(e: ErrorEvent, s: Sink[ErrorEvent]): Unit = {
// do some processing related to the event
// notify the event sink
s.notify(e)
}
A couple of hypothetical Sink implementations.
trait SystemEventSink extends Sink[SystemEvent]
val ses = new SystemEventSink {
override def notify(o: SystemEvent): Unit = ???
}
trait GenericEventSink extends Sink[Event]
val ges = new GenericEventSink {
override def notify(o: Event): Unit = ???
}
The following method calls are accepted by the compiler:
appEventFired(new ApplicationEvent {}, ses)
errorEventFired(new ErrorEvent {}, ges)
appEventFired(new ApplicationEvent {}, ges)
Looking at the series of calls you notice that it is possible to call a method expecting a Sink[ApplicationEvent] with a Sink[SystemEvent] and even with a Sink[Event]. Also, you can call the method expecting a Sink[ErrorEvent] with a Sink[Event].
By replacing invariance with a contravariance constraint, a Sink[SystemEvent] becomes a subtype of Sink[ApplicationEvent]. Therefore, contravariance can also be thought of as a ‘widening’ relationship, since types are ‘widened’ from more specific to more generic.
Conclusion
This example has been described in a series of articles about variance found on my blog
In the end, I think it helps to also understand the theory behind it...
Short answer that might help people who were super confused like me and didn't want to read these long winded examples:
Imagine you have 2 classes Animal, and Cat, which extends Animal. Now, imagine that you have a type Printer[Cat], that contains the functionality for printing Cats. And you have a method like this:
def print(p: Printer[Cat], cat: Cat) = p.print(cat)
but the thing is, that since Cat is an Animal, Printer[Animal] should also be able to print Cats, right?
Well, if Printer[T] were defined like Printer[-T], i.e. contravariant, then we could pass Printer[Animal] to the print function above and use its functionality to print cats.
This is why contravariance exists. Another example, from C#, for example, is the class IComparer which is contravariant as well. Why? Because we should be able to use Animal comparers to compare Cats, too.