Type Driven Development with Idris presents this program:
StringOrInt : Bool -> Type
StringOrInt x = case x of
True => Int
False => String
How can such a method be written in Scala?
Type Driven Development with Idris presents this program:
StringOrInt : Bool -> Type
StringOrInt x = case x of
True => Int
False => String
How can such a method be written in Scala?
Alexey's answer is good, but I think we can get to a more generalizable Scala rendering of this function if we embed it in a slightly larger context.
Edwin shows a use of StringOrInt
in the function valToString
,
valToString : (x : Bool) -> StringOrInt x -> String
valToString x val = case x of
True => cast val
False => val
In words, valToString
takes a Bool
first argument which fixes the type of its second argument as either Int
or String
and renders the latter as a String
as appropriate for its type.
We can translate this to Scala as follows,
sealed trait Bool
case object True extends Bool
case object False extends Bool
sealed trait StringOrInt[B, T] {
def apply(t: T): StringOrIntValue[T]
}
object StringOrInt {
implicit val trueInt: StringOrInt[True.type, Int] =
new StringOrInt[True.type, Int] {
def apply(t: Int) = I(t)
}
implicit val falseString: StringOrInt[False.type, String] =
new StringOrInt[False.type, String] {
def apply(t: String) = S(t)
}
}
sealed trait StringOrIntValue[T]
case class S(s: String) extends StringOrIntValue[String]
case class I(i: Int) extends StringOrIntValue[Int]
def valToString[T](x: Bool)(v: T)(implicit si: StringOrInt[x.type, T]): String =
si(v) match {
case S(s) => s
case I(i) => i.toString
}
Here we are using a variety of Scala's limited dependently typed features to encode Idris's full-spectrum dependent types.
True.type
and False.type
to cross from the value level to the type level.StringOrInt
as a type class indexed by the singleton Bool
types, each case of the Idris function being represented by a distinct implicit instance.valToString
as a Scala dependent method, allowing us to use the singleton type of the Bool
argument x
to select the implicit StringOrInt
instance si
, which in turn determines the type parameter T
which fixes the type of the second argument v
.valToString
by using the selected StringOrInt
instance to lift the v
argument into a Scala GADT which allows the Scala pattern match to refine the type of v
on the RHS of the cases.Exercising this on the Scala REPL,
scala> valToString(True)(23)
res0: String = 23
scala> valToString(False)("foo")
res1: String = foo
Lots of hoops to jump through, and lots of accidental complexity, nevertheless, it can be done.
This would be one approach (but it's a lot more limited than in Idris):
trait Type {
type T
}
def stringOrInt(x: Boolean) = // Scala infers Type { type T >: Int with String }
if (x) new Type { type T = Int } else new Type { type T = String }
and then to use it
def f(t: Type): t.T = ...
If you are willing to limit yourself to literals, you could use singleton types of true
and false
using Shapeless. From examples:
import syntax.singleton._
val wTrue = Witness(true)
type True = wTrue.T
val wFalse = Witness(false)
type False = wFalse.T
trait Type1[A] { type T }
implicit val Type1True: Type1[True] = new Type1[True] { type T = Int }
implicit val Type1False: Type1[False] = new Type1[False] { type T = String }
See also Any reason why scala does not explicitly support dependent types?, http://www.infoq.com/presentations/scala-idris and http://wheaties.github.io/Presentations/Scala-Dep-Types/dependent-types.html.
I noticed that getStringOrInt
example has not been implemented by either of the two answers
getStringOrInt : (x : Bool) -> StringOrInt x
getStringOrInt x = case x of
True => 10
False => "Hello"
I found Miles Sabin explanation very useful and this approach is based on his. I found it more intuitive to split GADT-like construction from Scala apply() trickery and try to map my code to Idris/Haskell concepts. I hope others find this useful. I am using GADT explicitly in names to emphasize the GADT-ness. The 2 ingredients of this code are: GADT concept and Scala's implicits.
Here is a slightly modified Miles Sabin's solution that implements both getStringOrInt
and valToString
.
sealed trait StringOrIntGADT[T]
case class S(s: String) extends StringOrIntGADT[String]
case class I(i: Int) extends StringOrIntGADT[Int]
/* this compiles with 2.12.6
before I would have to use ah-hoc polymorphic extract method */
def extractStringOrInt[T](t: StringOrIntGADT[T]) : T =
t match {
case S(s) => s
case I(i) => i
}
/* apply trickery gives T -> StringOrIntGADT[T] conversion */
sealed trait EncodeInStringOrInt[T] {
def apply(t: T): StringOrIntGADT[T]
}
object EncodeInStringOrInt {
implicit val encodeString : EncodeInStringOrInt[String] = new EncodeInStringOrInt[String]{
def apply(t: String) = S(t)
}
implicit val encodeInt : EncodeInStringOrInt[Int] = new EncodeInStringOrInt[Int]{
def apply(t: Int) = I(t)
}
}
/* Subtyping provides type level Boolean */
sealed trait Bool
case object True extends Bool
case object False extends Bool
/* Type level mapping between Bool and String/Int types.
Somewhat mimicking type family concept in type theory or Haskell */
sealed trait DecideStringOrIntGADT[B, T]
case object PickS extends DecideStringOrIntGADT[False.type, String]
case object PickI extends DecideStringOrIntGADT[True.type, Int]
object DecideStringOrIntGADT {
implicit val trueInt: DecideStringOrIntGADT[True.type, Int] = PickI
implicit val falseString: DecideStringOrIntGADT[False.type, String] = PickS
}
All this work allows me to implement decent versions of getStringOrInt
and valToString
def pickStringOrInt[B, T](c: DecideStringOrIntGADT[B, T]): StringOrIntGADT[T]=
c match {
case PickS => S("Hello")
case PickI => I(2)
}
def getStringOrInt[T](b: Bool)(implicit ev: DecideStringOrIntGADT[b.type, T]): T =
extractStringOrInt(pickStringOrInt(ev))
def valToString[T](b: Bool)(v: T)(implicit ev: EncodeInStringOrInt[T], de: DecideStringOrIntGADT[b.type, T]): String =
ev(v) match {
case S(s) => s
case I(i) => i.toString
}
All this (unfortunate) complexity appears to be needed, for example this will not compile
// def getStringOrInt2[T](b: Bool)(implicit ev: DecideStringOrIntGADT[b.type, T]): T =
// ev match {
// case PickS => "Hello"
// case PickI => 2
// }
I have a pet project in which I compared all code in the Idris book to Haskell. https://github.com/rpeszek/IdrisTddNotes/wiki (I am starting work on a Scala version of this comparison.)
With type-level Boolean (which is effectively what we have here) StringOrInt
examples become super simple if we have type families (partial functions between types). See bottom of
https://github.com/rpeszek/IdrisTddNotes/wiki/Part1_Sec1_4_5
This makes Haskell/Idris code is so much simpler and easier to read and understand.
Notice that valToString
matches on StringOrIntGADT[T]/StringOrIntValue[T]
constructors and not directly on Bool. That is one example where Idris and Haskell shine.