3

I'd like to construct a type like LimitedString[Limit] where Limit is a type representation of the maximum length of the string.

It would work along the lines of

class LimitedString[Limit] [private](val s: String)
object LimitedString {
  private def getLimit[Limit]: Int = ??? // turn `Limit` type into a value
  def truncate[Limit](s: String) = new LimitedString[Limit](s take getLimit)
  def get[Limit](s: String) = 
    if(s.length < getLimit) Some(new LimitedString[Limit](s))
    else None
}

type Str100 = LimitedString[100] // obviously this won't work
def someLibraryMethod(s: Str100) = { ... }

What I can't figure out is how to actually type (as in keyboard) the type (as in compilation) for Limit.

I started looking into Shapeless's singleton types and found that you can say

100.narrow
// res1: Int(100) = 100

But if I try to use Int(100) as the type, I get errors.

val x: Int(100) = 100
// error: ';' expected but '(' found.

Additionally, how would I implement something like def getLimit[Limit]: Int?

Dylan
  • 13,645
  • 3
  • 40
  • 67
  • Why not use Church encoding, through shapeless `Nat`. Representing numbers as types seems to be its exact purpose (for numbers < 400ish). If you need bigger, check [here](https://stackoverflow.com/questions/21296099/limits-of-nat-type-in-shapeless) for ideas/discussion on what to do with larger nums. – Davis Broda May 26 '17 at 17:36
  • 1
    I can't give a full answer at the moment, but take a look at Shapeless's `Witness`, which allows you to refer to types like this as e.g. `Witness.\`100\`.T` as well as providing access to their runtime values. – Travis Brown May 26 '17 at 17:49
  • @DavisBroda The intended Limit is in the neighborhood of 100. Would that mean I would have to write `Succ[Succ[Succ.....]]]` out to 100 levels in order to create my `Str100` type alias? – Dylan May 26 '17 at 17:49
  • I think there are aliases that simplify specifying number types. I think there are also ways to get the correct value implicitly in some cases. Unfortunately, I'm not too familiar with `Nat`. Thought I could point you - and other potential answerers - in the right direction, but lack the expertise to help with the specifics. – Davis Broda May 26 '17 at 17:54
  • @DavisBroda alright, thank you for the link. I'll take a deeper look if Travis's suggestion doesn't pan out (although it looks like it will) – Dylan May 26 '17 at 17:58

1 Answers1

2

I took @TravisBrown's suggestion to look into Shapless's Witness, and came up with this:

class LimitedString[Limit <: Int] private[util](val s: String) extends AnyVal {
    override def toString = s
}
class LimitedStringCompanion[Limit <: Int : Witness.Aux]{
    def limit: Int = implicitly[Witness.Aux[Limit]].value

    def unapply(s: String): Option[LimitedString[Limit]] = {
        if(s.length > limit) None else Some(new LimitedString(s))
    }

    def truncate(s: String): LimitedString[Limit] = new LimitedString(s take limit)
}

Usage:

import shapeless._

object MyLibraryThing {
  type Name = LimitedString[Witness.`50`.T]
  object Name extends LimitedStringCompanion[Witness.`50`.T]

  def rename(id: Int, name: Name) = { ... }
}

The key things that make it work:

  • Witness.Aux is a typeclass which you can use to get the singleton value back out of the type
  • The singleton type Witness.`50`.T is actually a subtype of Int
  • Type aliases make it more convenient to interact with
Dylan
  • 13,645
  • 3
  • 40
  • 67
  • One bummer about the "companion" is that the compiler won't automatically search it for implicits involving `Name` since `Name` is just a type alias. – Dylan May 30 '17 at 15:09