15

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?

Kevin Meredith
  • 41,036
  • 63
  • 209
  • 384

3 Answers3

16

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.

  • We use the singleton types True.type and False.type to cross from the value level to the type level.
  • We encode the function StringOrInt as a type class indexed by the singleton Bool types, each case of the Idris function being represented by a distinct implicit instance.
  • We write 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.
  • We encode the dependent pattern matching in the Idris 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.

Miles Sabin
  • 23,015
  • 6
  • 61
  • 95
  • Thank you for this detailed answer, Miles. Does using `shapeless` to answer this question provide any improvement to your answer? – Kevin Meredith Nov 25 '15 at 04:08
  • As Alexy mentioned in his answer, shapeless has facilities (eg. `Witness`) which make it easier to use the singleton types of primitive values, so you could use `Boolean` and `true` and `false` rather that the `Bool` and `True` and `False` that I invented above. That might be helpful in some practical contexts, but I don't think it adds anything from a general understanding point of view. – Miles Sabin Nov 26 '15 at 15:17
10

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.

Community
  • 1
  • 1
Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
  • 1
    I'm afraid I've just noticed that there's a problem with your `stringOrInt` definition: you've ascribed the type `Type` as the result ... this explicitly drops the refinements that you've computed in the body. OTOH, if you allow the result type to be inferred it will be `Type { type T >: Int with String }` irrespective of the value of `x`. – Miles Sabin Nov 22 '15 at 14:40
  • I think you've still not got a dependency on the value of `x` though ... the result type is `Type { type T >: Int with String }` irrespective of `x`. – Miles Sabin Nov 23 '15 at 13:07
2

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.

robert_peszek
  • 344
  • 2
  • 6
  • Any reason the code is different here https://github.com/rpeszek/IdrisTddScalaNotes/blob/master/src/main/scala/part1/Sec1_4_5_StringOrIntAsGADT.scala ? – Phil Sep 01 '19 at 08:04