Typelevel programming allows developers to encode several flavours of runtime invariants into the type system. Unfortunately, libraries that support typelevel programming tend to be poorly documented, difficult to understand and difficult to hack.
This makes them hard to customize to the needs of a specific project/problem. TypeQuux provides concise, efficient and easy-to-modify implementations of several typelevel programming primitives. As such, it represents collected wisdom on type-hackery in scala.
You can now:
Happy Hacking!
Here are two quick examples to illustrate the look, feel and abilities of the API.
scala> import typequux._ // importing the package
import typequux._
scala> import Typequux._ // useful default types
import Typequux._
scala> import constraint._ // importing the constraints API
import constraint._
scala> def atExample[R](i: LiteralHash[_], r: R)(implicit ev: AtConstraint[i.ValueHash, R, String]): String = ev(r)
atExample: [R](i: typequux.LiteralHash[_], r: R)(implicit ev: typequux.constraint.AtConstraint[i.ValueHash,R,String])String
A LiteralHash[T]
is an encapsulation of the singleton type associated with a literal of type T
. The ValueHash
is the
singleton type associated with the value of the literal. The companion object for LiteralHash
containt implicit converters
to build the objects from the regular literals. You can find out more here.
Constraints are typeclasses that abstract over specific typelevel datastructures like HLists, StringIndexedCollections and Records (and also programming techniques like structural induction) to encode the invariants associated with the problem. They are called constraints because one or more of the type parameters can be fixed to encode a specific condition. You can find out more here.
In this example, the encoded invariant is that given an index (ValueHash
) and an object of type R
, the value at the index position should be a String
.
It should be stressed that this is achieved without using runtime reflection or structural types.
This can be used with sequentially indexed collections, like HLists and Tuples (the regular scala tuples).
scala> atExample(1, true :+: "foo" :+: 42L :+: HNil) // this is a HList
res1: String = foo
scala> atExample(2, true :+: "foo" :+: 42L :+: HNil) // does not compile
scala> atExample(2, "oogachaka" :+: 3.14159 :+: "2.718" :+: Some("one is spying on you") :+: HNil)
res3: String = 2.718
scala> import TupleOps._ // pimp my tuples
import TupleOps._
scala> atExample(1, (true, "foo", 42L))
res4: String = foo
scala> atExample(2, (3.14159, Some("one is spying on you"), "oogachaka", 42L, List(1, 2,3)))
res6: String = oogachaka
The same code can also be used with string indexed collections, like StringIndexedCollections
(in which all elements are of the same type)
or Records
(in which they can be of different types). The type signatures for these can be quite complicated and dont communicate much.
For clarity, they are replaced below with \**\
.
scala> val si1 = SINil.add("name", "goku").add("son", "gohan").add("friend", "krillin") // String Indexed Collection
si1: \**\
scala> val si2 = SINil.add("leader", "Frieza").add("henchmen", "ginyu").add("father", "king cold")
si2: \**\
scala> atExample("name", si1)
res7: String = goku
scala> atExample("name", si2) // does not compile
scala> val si3 = SINil.add("house", "Lannister").add("name", "Tyrion").add("aka", "The Imp")
si3: \**\
scala> atExample("name", si3)
res10: String = Tyrion
scala> val r1 = RNil.add("name", "goku").add("powerlevel", 9000).add("friends", List("krillin", "yamcha")) // Record
r1: \**\
scala> val r2 = RNil.add("show", "futurama").add("coolest", "Zoidberg")
r2: \**\
scala> atExample("name", r1)
res11: String = goku
scala> atExample("name", r2) // does not compile
Or even classes
scala> case class User(name: String, age: Int)
defined class User
scala> atExample("name", Record.class2Record(User("Harshad", 24)))
res15: String = Harshad
Less sexy but as useful as the above example is the ability to perform arbitrary arity zips. Here are a few quick examples:
scala> (List(1, 2,3 ), List(1.1, 2.2, 4.4), List(true, false, false, true, true), List("philip", "fry", "bender", "leela")).azipped // tuples
res16: List[(Int, Double, Boolean, String)] = List((1,1.1,true,philip), (2,2.2,false,fry), (3,4.4,false,bender))
scala> (List(1, 2,3 ) :+: List(1.1, 2.2, 4.4) :+: List(true, false, false, true, true) :+: List("philip", "fry", "bender", "leela") :+: HNil).azipped // HLists
res17: \**\ = List(1 :+: 1.1 :+: true :+: philip :+: HNil, 2 :+: 2.2 :+: false :+: fry :+: HNil, 3 :+: 4.4 :+: false :+: bender :+: HNil)
scala> (Stream.from(1), Stream.continually(util.Random.nextBoolean)).zipwith((a: (Int, Boolean)) => if(a._2) a._1 * 100.0 else a._1 / 100.0)
res19: Stream[Double] = Stream(100.0, ?)
scala> (res19 take 5).toVector
res20: Vector[Double] = Vector(100.0, 200.0, 0.03, 400.0, 0.05)
The rest of the project documentation provides details on these and other primitives.
While scala is an advanced programming language and typelevel programming is an advanced aspect of scala, the library has been designed so as to be usable and modifiable without going into the gory-depths of the type system.
This section provides examples on the usage of the public APIs of the supplied primitives
Church booleans are the simplest typelevel constructs and are mostly used to establish invariants of more complex constructs. Functionally, they are an alias for a single type constructor that chooses from one of two alternatives.
sealed trait Bool {
type If [T <: Up, F <: Up, Up] <: Up
}
trait True extends Bool {
override type If[T <: Up, F <: Up, Up] = T
}
trait False extends Bool {
override type If[T <: Up, F <: Up, Up] = F
}
The usage is pretty straightforward
scala> import typequux._ // package
import typequux._
scala> import Typequux._ // useful imports
import Typequux._
scala> type Rep[B <: Bool] = B#If[Int, String, Any]
defined type alias Rep
scala> implicitly[Rep[True] =:= Int]
res0: =:=[Rep[typequux.typequux.True],Int] = <function1>
scala> implicitly[Rep[False] =:= String]
res1: =:=[Rep[typequux.typequux.False],String] = <function1>
The companion object implements type constructors for common operations. Supported operations are:
scala> import Bool._
import Bool._
scala> implicitly[False && False =:= False]
res2: =:=[typequux.Bool.&&[typequux.typequux.False,typequux.typequux.False],typequux.typequux.False] = <function1>
scala> implicitly[False && True =:= False]
res3: =:=[typequux.Bool.&&[typequux.typequux.False,typequux.typequux.True],typequux.typequux.False] = <function1>
scala> implicitly[True && False =:= False]
res4: =:=[typequux.Bool.&&[typequux.typequux.True,typequux.typequux.False],typequux.typequux.False] = <function1>
scala> implicitly[True && True =:= True]
res5: =:=[typequux.Bool.&&[typequux.typequux.True,typequux.typequux.True],typequux.typequux.True] = <function1>
scala> implicitly[False || False =:= False]
res6: =:=[typequux.Bool.||[typequux.typequux.False,typequux.typequux.False],typequux.typequux.False] = <function1>
scala> implicitly[True || False =:= True]
res7: =:=[typequux.Bool.||[typequux.typequux.True,typequux.typequux.False],typequux.typequux.True] = <function1>
scala> implicitly[False || True =:= True]
res8: =:=[typequux.Bool.||[typequux.typequux.False,typequux.typequux.True],typequux.typequux.True] = <function1>
scala> implicitly[True || True =:= True]
res9: =:=[typequux.Bool.||[typequux.typequux.True,typequux.typequux.True],typequux.typequux.True] = <function1>
scala> implicitly[Not[True] =:= False]
res10: =:=[typequux.Bool.Not[typequux.typequux.True],typequux.typequux.False] = <function1>
scala> implicitly[Not[False] =:= True]
res11: =:=[typequux.Bool.Not[typequux.typequux.False],typequux.typequux.True] = <function1>
scala> implicitly[Not[Not[True]] =:= True]
res12: =:=[typequux.Bool.Not[typequux.Bool.Not[typequux.typequux.True]],typequux.typequux.True] = <function1>
scala> implicitly[False Xor False =:= False]
res13: =:=[typequux.Bool.Xor[typequux.typequux.False,typequux.typequux.False],typequux.typequux.False] = <function1>
scala> implicitly[True Xor False =:= True]
res14: =:=[typequux.Bool.Xor[typequux.typequux.True,typequux.typequux.False],typequux.typequux.True] = <function1>
scala> implicitly[False Xor True =:= True]
res15: =:=[typequux.Bool.Xor[typequux.typequux.False,typequux.typequux.True],typequux.typequux.True] = <function1>
scala> implicitly[True Xor True =:= False]
res16: =:=[typequux.Bool.Xor[typequux.typequux.True,typequux.typequux.True],typequux.typequux.False] = <function1>
scala> implicitly[True ->> True =:= True]
res17: =:=[typequux.Bool.->>[typequux.typequux.True,typequux.typequux.True],typequux.typequux.True] = <function1>
scala> implicitly[True ->> False =:= False]
res18: =:=[typequux.Bool.->>[typequux.typequux.True,typequux.typequux.False],typequux.typequux.False] = <function1>
scala> implicitly[False ->> True =:= True]
res19: =:=[typequux.Bool.->>[typequux.typequux.False,typequux.typequux.True],typequux.typequux.True] = <function1>
scala> implicitly[False ->> False =:= True]
res20: =:=[typequux.Bool.->>[typequux.typequux.False,typequux.typequux.False],typequux.typequux.True] = <function1>
scala> implicitly[False Eqv False =:= True]
res21: =:=[typequux.Bool.Eqv[typequux.typequux.False,typequux.typequux.False],typequux.typequux.True] = <function1>
scala> implicitly[False Eqv True =:= False]
res22: =:=[typequux.Bool.Eqv[typequux.typequux.False,typequux.typequux.True],typequux.typequux.False] = <function1>
scala> implicitly[True Eqv False =:= False]
res23: =:=[typequux.Bool.Eqv[typequux.typequux.True,typequux.typequux.False],typequux.typequux.False] = <function1>
scala> implicitly[True Eqv True =:= True]
res24: =:=[typequux.Bool.Eqv[typequux.typequux.True,typequux.typequux.True],typequux.typequux.True] = <function1>
These type constructors can be used to prove properties about other constructs. For example, total order transitivity of dense numbers:
scala> class TotalOrderTransitivity[A, B, C]
defined class TotalOrderTransitivity
scala> import Dense._
import Dense._
scala> :paste
// Entering paste mode (ctrl-D to finish)
implicit def toToTr[A <: Dense, B <: Dense, C <: Dense](
implicit ev: =:=[True, &&[A <= B, B <= C] ->> <=[A, C]]): TotalOrderTransitivity[A, B, C] =
new TotalOrderTransitivity[A, B, C]
// Exiting paste mode, now interpreting.
toToTr: [A <: typequux.Dense, B <: typequux.Dense, C <: typequux.Dense](implicit ev: =:=[typequux.typequux.True,typequux.Bool.->>[typequux.Bool.&&[typequux.Dense.<=[A,B],typequux.Dense.<=[B,C]],typequux.Dense.<=[A,C]]])TotalOrderTransitivity[A,B,C]
scala> implicitly[TotalOrderTransitivity[_3, _0, _12]]
res25: TotalOrderTransitivity[typequux.Dense._3,typequux.Dense._0,typequux.Dense._12] = TotalOrderTransitivity@386d4a02
scala> implicitly[TotalOrderTransitivity[_3, _0, _2]]
res26: TotalOrderTransitivity[typequux.Dense._3,typequux.Dense._0,typequux.Dense._2] = TotalOrderTransitivity@4bfb88ce
scala> Bool.toBoolean[True]
res27: Boolean = true
scala> Bool.toBoolean[False]
res28: Boolean = false
Church booleans can be shown to satisfy the following laws:
||[A, B || C] =:= ||[A || B, C]
&&[A, B && C] =:= &&[A && B, C]
||[A, B] =:= ||[B, A]
&&[A, B] =:= &&[B, A]
||[A, B && C] =:= &&[A || B, A || C]
&&[A, B || C] =:= ||[A && B, A && C]
||[A, False] =:= A
&&[A, True] =:= A
||[A, True] =:= True
&&[A, False] =:= False
||[A, A] =:= A
&&[A, A] =:= A
&&[A, A || B] =:= A
||[A, A && B] =:= A
&&[A, Not[A]] =:= False
||[A, Not[A]] =:= True
Not[Not[A]] =:= A
&&[Not[A], Not[B]] =:= Not[A || B]
||[Not[A], Not[B]] =:= Not[A && B]
Many typelevel constructs like heterogenous lists (an in the case of this library, also type sets, type maps, literal singleton types and other constructs derived from them) rely on an encoding of natural numbers at the type level. Peano numbers are the simplest encodings of natural numbers - a numeric type is either 0 or a successor of another.
sealed trait Nat
trait Nat0 extends Nat
trait Succ[N <: Nat] extends Nat
object Nat {
type _0 = Nat0
type _1 = Succ[_0]
type _2 = Succ[_1]
type _3 = Succ[_2]
type _4 = Succ[_3]
type _5 = Succ[_4]
type _6 = Succ[_5]
type _7 = Succ[_6]
type _8 = Succ[_7]
type _9 = Succ[_8]
}
Finding the successor or the predecessor is a constant time operation for peano numbers which is why they are often used as indexers. However, they are cumbersome to construct (since every preceding number needs to be constructed first) and every other operation requires linear time or worse. This makes them unsuitable for most of the more complex constructs included in the library, which is why typequux exclusively uses dense numbers. A rich implementation of Peano numbers is provided nonetheless because it is an important primitive that may find utility in the code that you write.
The type constructers that are a part of the Nat
trait are unlikely to be useful in practice. The companion object provided type constructors that implement common operations. Supported operations are:
scala> import typequux._; import Nat._
import typequux._
import Nat._
scala> implicitly[_2 + _3 =:= _5]
res0: =:=[typequux.Nat.+[typequux.Nat._2,typequux.Nat._3],typequux.Nat._5] = <function1>
scala> implicitly[_4 + _0 =:= _4]
res1: =:=[typequux.Nat.+[typequux.Nat._4,typequux.Nat._0],typequux.Nat._4] = <function1>
scala> implicitly[_5 * _0 =:= _0]
res2: =:=[typequux.Nat.*[typequux.Nat._5,typequux.Nat._0],typequux.Nat._0] = <function1>
scala> implicitly[_1 * _6 =:= _6]
res3: =:=[typequux.Nat.*[typequux.Nat._1,typequux.Nat._6],typequux.Nat._6] = <function1>
scala> implicitly[_2 * _3 =:= _6]
res4: =:=[typequux.Nat.*[typequux.Nat._2,typequux.Nat._3],typequux.Nat._6] = <function1>
scala> implicitly[_6 ^ _0 =:= _1]
res5: =:=[typequux.Nat.^[typequux.Nat._6,typequux.Nat._0],typequux.Nat._1] = <function1>
scala> implicitly[_7 ^ _1 =:= _7]
res6: =:=[typequux.Nat.^[typequux.Nat._7,typequux.Nat._1],typequux.Nat._7] = <function1>
scala> implicitly[_1 ^ _4 =:= _1]
res8: =:=[typequux.Nat.^[typequux.Nat._1,typequux.Nat._4],typequux.Nat._1] = <function1>
scala> implicitly[_2 ^ _3 =:= _8]
res10: =:=[typequux.Nat.^[typequux.Nat._2,typequux.Nat._3],typequux.Nat._8] = <function1>
scala> implicitly[_3 ^ _2 =:= _9]
res11: =:=[typequux.Nat.^[typequux.Nat._3,typequux.Nat._2],typequux.Nat._9] = <function1>
scala> implicitly[Fact[_0] =:= _1]
res12: =:=[typequux.Nat.Fact[typequux.Nat._0],typequux.Nat._1] = <function1>
scala> implicitly[Fact[_1] =:= _1]
res13: =:=[typequux.Nat.Fact[typequux.Nat._1],typequux.Nat._1] = <function1>
scala> implicitly[Fact[_3] =:= _6]
res14: =:=[typequux.Nat.Fact[typequux.Nat._3],typequux.Nat._6] = <function1>
scala> import Typequux._ // useful imports for True, False
import Typequux._
scala> implicitly[_0 < _3 =:= True]
res15: =:=[typequux.Nat.<[typequux.Nat._0,typequux.Nat._3],typequux.True] = <function1>
scala> implicitly[_5 <= _5 =:= True]
res16: =:=[typequux.Nat.<=[typequux.Nat._5,typequux.Nat._5],typequux.True] = <function1>
scala> implicitly[_5 === _5 =:= True]
res17: =:=[typequux.Nat.===[typequux.Nat._5,typequux.Nat._5],typequux.True] = <function1>
scala> implicitly[_6 >= _4 =:= True]
res18: =:=[typequux.Nat.>=[typequux.Nat._6,typequux.Nat._4],typequux.True] = <function1>
scala> implicitly[_6 > _4 =:= True]
res19: =:=[typequux.Nat.>[typequux.Nat._6,typequux.Nat._4],typequux.True] = <function1>
It is implemented by a marker typeclass. The names of the anonymous classes generated are not particularly
interesting and confuse the templating engine for the website. Thereofore, they are replaced below by \**\
scala> implicitly[NatDiff[_0, _0, _0]]
res22: typequux.NatDiff[typequux.Nat._0,typequux.Nat._0,typequux.Nat._0] = \**\
scala> implicitly[NatDiff[_3, _0, _3]]
res24: typequux.NatDiff[typequux.Nat._3,typequux.Nat._0,typequux.Nat._3] = \**\
scala> implicitly[NatDiff[_6, _2, _4]]
res25: typequux.NatDiff[typequux.Nat._6,typequux.Nat._2,typequux.Nat._4] = \**\
scala> Nat.toInt[_0]
res26: Int = 0
scala> Nat.toInt[_4]
res27: Int = 4
scala> Nat.toInt[_5 + _9]
res28: Int = 14
scala> Nat.toInt[_5 * _9]
res29: Int = 45
Peano numbers can be shown to satisfy:
+[A, B] =:= +[B, A]
*[A, B] =:= *[B, A]
+[A, +[B, C]] =:= +[+[A, B], C]
*[A, *[B, C]] =:= *[*[A, B], C]
+[A * C, B * C] =:= *[A + B, C]
+[A, _0] =:= A, +[_0, A] =:= A
*[A, _1] =:= A, *[_1, A] =:= A
^[A, _0] =:= _1
^[A, _1] =:= A
^[_1, A] =:= _1
Many typelevel constructs like heterogenous lists (and in this library, also type sets, type maps, literal singleton types and other constructs derived from them) rely on an encoding of natural numbers at the type level. While Peano numbers are simple, their construction is too cumbersome and performance too poor for them to be suitable for most of the more complex constructs provided by the library.
Functionally, dense numbers encode a numeric type in binary. A number is either 0 or a heterogenous list of digits. For non zero numbers, the head
of the list is the least significant bit and the last element is always 1
.
sealed trait Dense
trait DCons[d <: Dense.Digit, T <: Dense] extends Dense
trait DNil extends Dense
object Dense {
sealed trait Digit
trait D0 extends Digit
trait D1 extends Digit
type ::[H <: Digit, T <: Dense] = DCons[H, T]
type _0 = DNil
type _1 = D1 :: DNil
type _2 = D0 :: D1 :: DNil
type _3 = D1 :: D1 :: DNil
type _4 = D0 :: D0 :: D1 :: DNil
type _5 = D1 :: D0 :: D1 :: DNil
type _6 = D0 :: D1 :: D1 :: DNil
type _7 = D1 :: D1 :: D1 :: DNil
type _8 = D0 :: D0 :: D0 :: D1 :: DNil
type _9 = D1 :: D0 :: D0 :: D1 :: DNil
}
By construction, left shift and right shift are constant time operations while all other times are log-time or worse. Therefore, in theory, indexers that use dense numbers are less efficient than those that use Peano numbers. In practice, the difference can be ignored and is compensated several times over by the performance gained in other operations. This is why typequux exclusively uses dense numbers to encode natural numbers into types.
The type signatures for dense numbers can be quite long and are not terribly informative. /**/
represents an omitted (for clarity) type signature.
Some of the type constructors that are a part of the Dense
trait are useful in practice.
scala> import typequux._; import Dense._
import typequux._
import Dense._
scala> implicitly[_0#Inc =:= _1]
res0: /**/ = <function1>
scala> implicitly[_7#Inc =:= _8]
res2: /**/ = <function1>
scala> implicitly[_1#Dec =:= _0]
res3: /**/ = <function1>
scala> implicitly[_16#Dec =:= _15]
res4: /**/ = <function1>
scala> implicitly[_0#ShiftL =:= _0]
res5: /**/ = <function1>
scala> implicitly[_1#ShiftL =:= _2]
res6: /**/ = <function1>
scala> implicitly[_3#ShiftL =:= _6]
res7: /**/ = <function1>
scala> implicitly[_0#ShiftR =:= _0]
res8: /**/ = <function1>
scala> implicitly[_1#ShiftR =:= _0]
res9: /**/ = <function1>
scala> implicitly[_2#ShiftR =:= _1]
res10: /**/ = <function1>
scala> implicitly[_7#ShiftR =:= _3]
res11: /**/ = <function1>
The companion object provides type constructors that implement common operations. Supported operations are:
scala> implicitly[_2 + _3 =:= _5]
res12: /**/ = <function1>
scala> implicitly[_6 + _0 =:= _6]
res13: /**/ = <function1>
scala> implicitly[_0 + _2 =:= _2]
res14: /**/ = <function1>
scala> implicitly[_4 * _0 =:= _0]
res15: =:=[typequux.Dense.*[typequux.Dense._4,typequux.Dense._0],typequux.Dense._0] = <function1>
scala> implicitly[_7 * _1 =:= _7]
res16: =:=[typequux.Dense.*[typequux.Dense._7,typequux.Dense._1],typequux.Dense._7] = <function1>
scala> implicitly[_3 * _4 =:= _12]
res17: =:=[typequux.Dense.*[typequux.Dense._3,typequux.Dense._4],typequux.Dense._12] = <function1>
scala> implicitly[_4 * _4 =:= _16]
res18: =:=[typequux.Dense.*[typequux.Dense._4,typequux.Dense._4],typequux.Dense._16] = <function1>
scala> implicitly[_5 ^ _0 =:= _1]
res19: /**/ = <function1>
scala> implicitly[_7 ^ _1 =:= _7]
res20: /**/ = <function1>
scala> implicitly[_1 ^ _4 =:= _1]
res21: /**/ = <function1>
scala> implicitly[_3 ^ _2 =:= _9]
res22: /**/ = <function1>
scala> implicitly[_2 ^ _4 =:= _16]
res23: /**/ = <function1>
scala> import Typequux._ // useful imports for True and False
import Typequux._
scala> implicitly[_0 < _3 =:= True]
res26: /**/ = <function1>
scala> implicitly[_5 <= _5 =:= True]
res27: /**/ = <function1>
scala> implicitly[_5 === _5 =:= True]
res28: /**/ = <function1>
scala> implicitly[_5 === _7 =:= False]
res29: /**/ = <function1>
scala> implicitly[_7 > _4 =:= True]
res30: /**/ = <function1>
scala> implicitly[_7 >= _4 =:= True]
res31: /**/ = <function1>
scala> implicitly[Max[_4, _5] =:= _5]
res32: /**/ = <function1>
scala> implicitly[Min[_5, _9] =:= _5]
res33: /**/ = <function1>
It is implemented by a marker typeclass.
scala> implicitly[DenseDiff[_0, _0, _0]]
res34: /**/ = /**/
scala> implicitly[DenseDiff[_4, _0, _4]]
res35: /**/ = /**/
scala> implicitly[DenseDiff[_13, _5, _8]]
res36: /**/ = /**/
scala> Dense.toLong[_0]
res37: Long = 0
scala> Dense.toLong[_16]
res38: Long = 16
scala> Dense.toLong[_16 * _4]
res39: Long = 64
scala> Dense.toInt[_0]
res40: Int = 0
scala> Dense.toInt[_16]
res41: Int = 16
scala> Dense.toInt[_16 * _4]
res42: Int = 64
Dense numbers can be shown to satisfy:
+[A, B] =:= +[B, A]
*[A, B] =:= *[B, A]
+[A, +[B, C]] =:= +[+[A, B], C]
*[A, *[B, C]] =:= *[*[A, B], C]
*[+[A, B], C] =:= +[*[A, C], *[B, C]]
+[A, _0] =:= A
, +[_0, A] =:= A
*[A, _1] =:= A
, *[_1, A] =:= A
A#Inc#Dec =:= A
A#ShiftL#ShiftR =:= A
A#ShiftL =:= *[A, _2]
Xor[===[A, _2 * A#ShiftR], ===[A, *[_2, A#ShiftR] + _1]] =:= True
*[A ^ B, A ^ C] =:= ^[A, B + C]
^[A ^ B, C] =:= ^[A, B * C]
*[A ^ C, B ^ C] =:= ^[A * B, C]
^[A, _1] =:= A
^[A, _0] =:= _1
^[_1, A] =:= _1
Type sets are implemented as binary trees of dense numbers.
sealed trait DenseSet
trait EmptyDenseSet extends DenseSet
trait NonEmptyDenseSet[V <: Dense, L <: DenseSet, R <: DenseSet] extends DenseSet
You can use the type constructors that are a part of the DenseSet trait or (more conveniently), use the aliases defined in the companion object. Supported operations are:
scala> import typequux._; import Typequux._; import Dense._; import DenseSet._
import typequux._
import Typequux._
import Dense._
import DenseSet._
scala> type S1 = EmptyDenseSet Include _7 Include _5 Include _2 Include _11
defined type alias S1
scala> type S2 = EmptyDenseSet Include _7 Include _2 Include _5 Include _7
defined type alias S2
scala> type S3 = EmptyDenseSet#Include[_7]#Include[_7]#Include[_2]#Include[_5]
defined type alias S3
scala> implicitly[S1 Contains _7 =:= True]
res0: =:=[typequux.DenseSet.Contains[S1,typequux.Dense._7],typequux.True] = <function1>
scala> implicitly[S2 Contains _7 =:= True]
res1: =:=[typequux.DenseSet.Contains[S2,typequux.Dense._7],typequux.True] = <function1>
scala> implicitly[S2 Contains _0 =:= False]
res3: =:=[typequux.DenseSet.Contains[S2,typequux.Dense._0],typequux.False] = <function1>
scala> type S4 = S2 Remove _7
defined type alias S4
scala> implicitly[S2 Contains _7 =:= True]
res8: =:=[typequux.DenseSet.Contains[S2,typequux.Dense._7],typequux.True] = <function1>
scala> implicitly[S4 Contains _7 =:= False]
res9: =:=[typequux.DenseSet.Contains[S4,typequux.Dense._7],typequux.False] = <function1>
scala> type S5 = EmptyDenseSet Include _5 Include _11 Include _2 Include _18
defined type alias S5
scala> type S6 = S2 Union S5
defined type alias S6
scala> implicitly[S6 Contains _5 =:= True]
res10: =:=[typequux.DenseSet.Contains[S6,typequux.Dense._5],typequux.True] = <function1>
scala> implicitly[S6 Contains _2 =:= True]
res11: =:=[typequux.DenseSet.Contains[S6,typequux.Dense._2],typequux.True] = <function1>
scala> implicitly[S6 Contains _7 =:= True]
res12: =:=[typequux.DenseSet.Contains[S6,typequux.Dense._7],typequux.True] = <function1>
scala> implicitly[S6 Contains _18 =:= True]
res13: =:=[typequux.DenseSet.Contains[S6,typequux.Dense._18],typequux.True] = <function1>
scala> implicitly[S6 Contains _11 =:= True]
res14: =:=[typequux.DenseSet.Contains[S6,typequux.Dense._11],typequux.True] = <function1>
The type of the dense number is not terribly informative. /**/
is a type signature omitted for clarity.
cala> implicitly[S1#Size =:= _4]
res15: /**/ = <function1>
scala> implicitly[S2#Size =:= _3]
res16: /**/ = <function1>
scala> implicitly[S4#Size =:= _2]
res17: /**/ = <function1>
scala> implicitly[S5#Size =:= _4]
res18: /**/ = <function1>
scala> implicitly[S6#Size =:= _5]
res19: /**/ = <function1>
scala> implicitly[EmptyDenseSet Eq EmptyDenseSet =:= True]
res20: =:=[typequux.DenseSet.Eq[typequux.EmptyDenseSet,typequux.EmptyDenseSet],typequux.True] = <function1>
scala> implicitly[EmptyDenseSet Eq S1 =:= False]
res21: =:=[typequux.DenseSet.Eq[typequux.EmptyDenseSet,S1],typequux.False] = <function1>
scala> implicitly[S2 Eq S3 =:= True]
res23: =:=[typequux.DenseSet.Eq[S2,S3],typequux.True] = <function1>
scala> implicitly[S1 Eq S2 =:= False]
res24: =:=[typequux.DenseSet.Eq[S1,S2],typequux.False] = <function1>
Since the specific type signature of a dense set depends on the path by which it was constructed, =:=
should not be used to check whether two sets are equivalent. You should use the equivalence type constructor described above.
scala> type Eqv1 = EmptyDenseSet Include _3 Include _2 Include _9 Include _4 Include _16
defined type alias Eqv1
scala> type Eqv2 = EmptyDenseSet Include _2 Include _3 Include _4 Include _9 Include _16
defined type alias Eqv2
scala> implicitly[Eqv1 =:= Eqv2]
<console>:26: error: Cannot prove that Eqv1 =:= Eqv2.
implicitly[Eqv1 =:= Eqv2]
^
scala> implicitly[Eqv1 Eq Eqv2 =:= True]
res26: =:=[typequux.DenseSet.Eq[Eqv1,Eqv2],typequux.True] = <function1>
Dense sets can be shown to satisfy:
True =:= Eq[X, X Union X]
True =:= Eq[X Union Y, Y Union X]
True =:= Eq[Union[X, Union[Y, Z]], Union[Union[X, Y], Z]]
Type maps are implemented as binary trees of key-value pairs in which the keys are dense numbers.
sealed trait DenseMap
trait EmptyDenseMap extends DenseMap
trait NonEmptyDenseMap[KT <: Dense, VT, L <: DenseMap, R <: DenseMap] extends DenseMap
You can use the type constructors that are a part of the DenseMap trait or (more conveniently), use the aliases defined in the companion object. Supported operations are:
scala> import typequux._; import Typequux._; import Dense._; import DenseMap._
import typequux._
import Typequux._
import Dense._
import DenseMap._
scala> type G = EmptyDenseMap#Add[_5, Int]#Add[_6, String]#Add[_2, List[Int]]#Add[_9, Option[Long]]#Add[_4, Unit]
defined type alias G
scala> type H = EmptyDenseMap#Add[_2, Int]#Add[_4, List[Int]]#Add[_0, None.type]
defined type alias H
scala> implicitly[G Contains _5 =:= True]
res0: =:=[typequux.DenseMap.Contains[G,typequux.Dense._5],typequux.True] = <function1>
scala> implicitly[H Contains _7 =:= False]
res1: =:=[typequux.DenseMap.Contains[H,typequux.Dense._7],typequux.False] = <function1>
scala> implicitly[H Contains _0 =:= True]
res2: =:=[typequux.DenseMap.Contains[H,typequux.Dense._0],typequux.True] = <function1>
scala> implicitly[G Get _2 =:= List[Int]]
res3: =:=[typequux.DenseMap.Get[G,typequux.Dense._2],List[Int]] = <function1>
scala> implicitly[G Get _4 =:= Unit]
res4: =:=[typequux.DenseMap.Get[G,typequux.Dense._4],Unit] = <function1>
scala> implicitly[H Get _0 =:= None.type]
res5: =:=[typequux.DenseMap.Get[H,typequux.Dense._0],None.type] = <function1>
scala> type GR = G Remove _6
defined type alias GR
scala> implicitly[G Contains _6 =:= True]
res6: =:=[typequux.DenseMap.Contains[G,typequux.Dense._6],typequux.True] = <function1>
scala> implicitly[GR Contains _6 =:= False]
res8: =:=[typequux.DenseMap.Contains[GR,typequux.Dense._6],typequux.False] = <function1>
scala> type J = G Union H
defined type alias J
scala> implicitly[J Contains _9 =:= True]
res9: =:=[typequux.DenseMap.Contains[J,typequux.Dense._9],typequux.True] = <function1>
scala> implicitly[J Contains _0 =:= True]
res10: =:=[typequux.DenseMap.Contains[J,typequux.Dense._0],typequux.True] = <function1>
scala> type GKS = G#Keyset
defined type alias GKS
scala> type HKS = H#Keyset
defined type alias HKS
scala> import DenseSet._
import DenseSet._
scala> type GKST = EmptyDenseSet Include _5 Include _6 Include _2 Include _9 Include _4
defined type alias GKST
scala> type HKST = EmptyDenseSet Include _2 Include _4 Include _0
defined type alias HKST
scala> implicitly[GKS Eq GKST =:= True]
res11: =:=[typequux.DenseSet.Eq[GKS,GKST],typequux.True] = <function1>
scala> implicitly[HKS Eq HKST =:= True]
res12: =:=[typequux.DenseSet.Eq[HKS,HKST],typequux.True] = <function1>
The type of the dense number is not terribly informative. /**/
is a type signature omitted for clarity.
scala> implicitly[G#Size =:= _5]
res13: /**/ = <function1>
scala> implicitly[H#Size =:= _3]
res14:/**/ = <function1>
scala> implicitly[J#Size =:= _6]
res17: /**/ = <function1>
Since the specific type signature of a dense map depends on the path by which it was constructed, =:=
should not be used to check whether two dense maps are equivalent. For the same reason, if you take the union of two dense sets that have a different value associated with the same key, the value associated with a key in the resultant map will depend on the history of the two maps. Therefore, it is not possible to establish the sort of general laws regarding the union of two dense maps as it was for dense sets.
In the library, DenseMaps are used as backing datastructures for StringIndexedCollections and Records.
Ordinary scala functions are monomorphic, which is to say that they transform values of one type to those of another type.
The requirements of several problems are however to transform contexts, form example, from a List
to an Option
, or from
an open connection to a closed connection. Natural transformations encode these transformations whilst maintaining the type
of the object in the context.
trait ~>[-F[_], +G[_]] {
def apply[A](a: F[A]): G[A]
}
Since the names of anonymous classes are not particularly interesting and dollar signs confuse the templating engine
used to produce the documentation, the names of anonymous classes are replaced by /**/
below.
Supported operations are:
scala> import typequux._; // package
import typequux._
scala> import Typequux._ // useful imports
import Typequux._
scala> val singletonList = new (Id ~> List){override def apply[T](t: T) = List(t)}
singletonList: typequux.~>[typequux.typequux.Id,List] = /**/
scala> val list2Option = new (List ~> Option){override def apply[T](x: List[T]) = x.headOption}
list2Option: typequux.~>[List,Option] = /**/
scala> singletonList(12)
res0: List[Int] = List(12)
scala> singletonList("oogachaka")
res1: List[String] = List(oogachaka)
scala> singletonList((true, 42))
res2: List[(Boolean, Int)] = List((true,42))
scala> list2Option(List(1, 2, 3))
res3: Option[Int] = Some(1)
scala> list2Option(List())
res4: Option[Nothing] = None
scala> val list2Set = new (List ~> Set) {override def apply[T](x: List[T]) = x.toSet}
list2Set: typequux.~>[List,Set] = /**/
scala> val singletonSet1 = singletonList andThen list2Set
singletonSet1: typequux.~>[typequux.typequux.Id,Set] = /**/
scala> val singletonSet2 = list2Set compose singletonList
singletonSet2: typequux.~>[typequux.typequux.Id,Set] = /**/
scala> singletonSet1(42)
res5: Set[Int] = Set(42)
scala> singletonSet1(true)
res6: Set[Boolean] = Set(true)
scala> singletonSet2("goku")
res7: Set[String] = Set(goku)
scala> Vector(List(1, 2,3), List(100, 200, 100)) map list2Option
res9: scala.collection.immutable.Vector[Option[Int]] = Vector(Some(1), Some(100))
scala> Vector(List(1, 2,3), List(100, 200, 100)) map list2Set
res10: scala.collection.immutable.Vector[Set[Int]] = Vector(Set(1, 2, 3), Set(100, 200))
Type Unions, Type Exclusions, Type Hierarchy Unions and Type Hierarchy Exclusions can be used to get around compramises that arise when building subtype hierarchies. While their usage can break the Liskov Substitution Principle, this is often pragmatically preferrable to the code breaking at runtime.
Type unions can be used to guarentee that a given type is a member of the supplied HList of types.
scala> import typequux._ // package
import typequux._
scala> import Typequux._ // useful imports
import Typequux._
scala> implicitly[Contained[Int, Int :+: HNil]]
res0: typequux.Contained[Int,typequux.typequux.:+:[Int,typequux.typequux.HNil]] = typequux.Contained@17a63316
scala> implicitly[Contained[Long, Int :+: String :+: Long :+: HNil]]
res1: typequux.Contained[Long,typequux.typequux.:+:[Int,typequux.typequux.:+:[String,typequux.typequux.:+:[Long,typequux.typequux.HNil]]]] = typequux.Contained@17f0c710
scala> implicitly[Contained[Option[_], List[_] :+: Option[_] :+: Set[_] :+: HNil]]
res2: typequux.Contained[Option[_],typequux.HList.HCons[List[_],typequux.HList.HCons[Option[_],typequux.HList.HCons[scala.collection.immutable.Set[_],typequux.typequux.HNil]]]] = typequux.Contained@1edf0567
scala> implicitly[Contained[Double, Int :+: String :+: Long :+: HNil]] // does not compile
scala> :paste
// Entering paste mode (ctrl-D to finish)
def containedFunc[T](x: T)(implicit ev: Contained[T, Int :+: String :+: HNil]): Unit = x match{
case _: Int => println("got int")
case _: String => println("got string")
}
// Exiting paste mode, now interpreting.
containedFunc: [T](x: T)(implicit ev: typequux.Contained[T,typequux.typequux.:+:[Int,typequux.typequux.:+:[String,typequux.typequux.HNil]]])Unit
scala> containedFunc(42)
got int
scala> containedFunc("oogachaka")
got string
scala> containedFunc(12.12) // does not compile
Type exclusions can be used to guarentee that a given type is not a member of the supplied HList of types.
scala> implicitly[NotContained[Int, HNil]]
res7: typequux.NotContained[Int,typequux.typequux.HNil] = typequux.NotContained@fda0097
scala> implicitly[NotContained[Int, String :+: Long :+: HNil]]
res8: typequux.NotContained[Int,typequux.typequux.:+:[String,typequux.typequux.:+:[Long,typequux.typequux.HNil]]] = typequux.NotContained@26c36dfd
scala> implicitly[NotContained[List[_], Set[_] :+: Option[_] :+: HNil]]
res9: typequux.NotContained[List[_],typequux.HList.HCons[scala.collection.immutable.Set[_],typequux.HList.HCons[Option[_],typequux.typequux.HNil]]] = typequux.NotContained@c3f3d1a
scala> implicitly[NotContained[String, String :+: Long :+: HNil]] // does not compile
Type Hierarchy Unions can be used to guarentee that a given type is a subtype of one of the supplied HList types.
scala> implicitly[SubType[Int, AnyVal :+: AnyRef :+: HNil]]
res11: typequux.SubType[Int,typequux.typequux.:+:[AnyVal,typequux.typequux.:+:[AnyRef,typequux.typequux.HNil]]] = typequux.SubType@4da701c0
scala> implicitly[SubType[List[Int], List[AnyVal] :+: Set[AnyRef] :+: HNil]] // since lists are covariant
res12: typequux.SubType[List[Int],typequux.typequux.:+:[List[AnyVal],typequux.typequux.:+:[Set[AnyRef],typequux.typequux.HNil]]] = typequux.SubType@6bfda438
scala> implicitly[SubType[List[Int], Traversable[_] :+: Option[_] :+: HNil]]
res13: typequux.SubType[List[Int],typequux.HList.HCons[Traversable[_],typequux.HList.HCons[Option[_],typequux.typequux.HNil]]] = typequux.SubType@270e9e6
scala> implicitly[SubType[List[_], Option[_] :+: Array[_] :+: HNil]] // does not compile
Type Hierarchy Exclusions can be used to guarentee that a given type is not a subtype of the supplied HList of types.
scala> implicitly[NotSubType[Int, HNil]]
res15: typequux.NotSubType[Int,typequux.typequux.HNil] = typequux.NotSubType@7bc345e9
scala> implicitly[NotSubType[Traversable[_], List[_] :+: Set[_] :+: HNil]]
res16: typequux.NotSubType[Traversable[_],typequux.HList.HCons[List[_],typequux.HList.HCons[scala.collection.immutable.Set[_],typequux.typequux.HNil]]] = typequux.NotSubType@5f65f7a0
scala> implicitly[NotSubType[Array[Int], Array[Any] :+: List[_] :+: HNil]] // since arrays are invariant
res17: typequux.NotSubType[Array[Int],typequux.HList.HCons[Array[Any],typequux.HList.HCons[List[_],typequux.typequux.HNil]]] = typequux.NotSubType@13ebb85
scala> implicitly[NotSubType[List[_], Traversable[_] :+: Option[_] :+: HNil]] // does not compile
Singleton types for Literals are implemented using a marker trait called a LiteralHash
.
trait LiteralHash[X] {
type TypeHash <: Dense
type ValueHash <: Dense
val value: X
}
The TypeHash
is a natural number representing the type of the literal and the ValueHash
is a natural number representing
the value of the literal. This way, literals that have the same truncated binary representation, like 42
, 42L
and
-2147483606
have different TypeHash
s and are therefore differentiable.
The companion object contains implicit conversions to build a LiteralHash[X]
from a literal of type X
. Usage is
very straightforward. For example,
scala> import typequux._
import typequux._
scala> import Dense._
import Dense._
scala> def lh(i: LiteralHash[Int])(implicit ev0: DenseRep[i.ValueHash], ev1: DenseRep[i.TypeHash]) = (ev0.v, ev1.v)
lh: (i: typequux.LiteralHash[Int])(implicit ev0: typequux.Dense.DenseRep[i.ValueHash], implicit ev1: typequux.Dense.DenseRep[i.TypeHash])(Long, Long)
scala> lh(12)
res0: (Long, Long) = (12,9)
scala> lh(15)
res1: (Long, Long) = (15,9)
scala> lh(15000)
res2: (Long, Long) = (15000,9)
scala> lh(-15000)
res3: (Long, Long) = (2147468648,8)
scala> def lh2(i: LiteralHash[Int], j: LiteralHash[Int])(implicit ev: DenseRep[i.ValueHash * j.ValueHash]) = ev.v
lh2: (i: typequux.LiteralHash[Int], j: typequux.LiteralHash[Int])(implicit ev: typequux.Dense.DenseRep[typequux.Dense.*[i.ValueHash,j.ValueHash]])Long
scala> lh2(12, 10)
res4: Long = 120
scala> lh2(14, 2)
res5: Long = 28
TypeQuux ships with a rich API for operating on heterogenous lists and subsumes operations typically associated with klists. Operations that would result in runtime errors (mostly) do not compile. All indexed operations (apply
, drop
, take
etc) are 0-based and can be indexed from the left or the right.
The type signatures for hlists can be quite long and are not terribly informative in the examples presented below. /**/
represents an omitted (for clarity) type signature.
scala> import typequux._ // package
import typequux._
scala> import Typequux._ // useful imports
import Typequux._
scala> type LT = String :+: AnyVal :+: AnyRef :+: Traversable[_] :+: Option[Int] :+: HNil
defined type alias LT
scala> type ST = String :+: Long :+: Set[Double] :+: List[Int] :+: None.type :+: HNil
defined type alias ST
scala> implicitly[ST <:< LT] // compiles
res0: <:<[ST,LT] = <function1>
scala> val a = "mmm" :+: 42L :+: Set(3.14, 2.718) :+: List(1, 2, 3) :+: None :+: HNil
a: /**/ = mmm :+: 42 :+: Set(3.14, 2.718) :+: List(1, 2, 3) :+: None :+: HNil
scala> val l: LT = a
l: LT = mmm :+: 42 :+: Set(3.14, 2.718) :+: List(1, 2, 3) :+: None :+: HNil
scala> val s: ST = a
s: ST = mmm :+: 42 :+: Set(3.14, 2.718) :+: List(1, 2, 3) :+: None :+: HNil
scala> val a = 3 :+: "ai4" :+: List('r', 'h') :+: HNil; val b = '3' :+: 2 :+: 'j' :+: "sdfh" :+: HNil
a: /**/ = 3 :+: ai4 :+: List(r, h) :+: HNil
b: /**/ = 3 :+: 2 :+: j :+: sdfh :+: HNil
scala> val ab = a :++: b
ab: /**/ = 3 :+: ai4 :+: List(r, h) :+: 3 :+: 2 :+: j :+: sdfh :+: HNil
scala> val x = "str" :+: true :+: 1 :+: Some(3.14) :+: HNil
x: /**/ = str :+: true :+: 1 :+: Some(3.14) :+: HNil
scala> val xr = x.reverse
xr: /**/ = Some(3.14) :+: 1 :+: true :+: str :+: HNil
scala> val xrr = xr.reverse
xrr: /**/ = str :+: true :+: 1 :+: Some(3.14) :+: HNil
scala> x == xrr
res26: Boolean = true
scala> val hl1 = true :+: "foo" :+: 2 :+: Some("one is waiting for you") :+: HNil
hl1: /**/ = true :+: foo :+: 2 :+: Some(one is waiting for you) :+: HNil
scala> hl1.length
res1: Long = 4
scala> HNil.length
res2: Long = 0
scala> val p = 3 :+: true :+: "asdf" :+: false :+: 'k' :+: () :+: 13 :+: 9.3 :+: HNil
p: /**/ = 3 :+: true :+: asdf :+: false :+: k :+: () :+: 13 :+: 9.3 :+: HNil
scala> p(0) // note that the type information is preserved
res3: Int = 3
scala> p(4)
res4: Char = k
scala> p.right(0)
res5: Double = 9.3
scala> p.right(6)
res6: Boolean = true
scala> p(100) // does not compile
The arguments is the number of elements to drop.
scala> p.drop(0)
res7: /**/ = 3 :+: true :+: asdf :+: false :+: k :+: () :+: 13 :+: 9.3 :+: HNil
scala> p.drop(4)
res8: /**/ = k :+: () :+: 13 :+: 9.3 :+: HNil
scala> p.dropRight(4)
res10: /**/ = 3 :+: true :+: asdf :+: false :+: HNil
scala> p.dropRight(6)
res12: /**/ = 3 :+: true :+: HNil
The argument is the number of elements to take.
scala> p.take(0)
res13: /**/ = HNil
scala> p.take(4)
res14: /**/ = 3 :+: true :+: asdf :+: false :+: HNil
scala> p.takeRight(4)
res15: /**/ = k :+: () :+: 13 :+: 9.3 :+: HNil
scala> p.takeRight(8)
res16: /**/ = 3 :+: true :+: asdf :+: false :+: k :+: () :+: 13 :+: 9.3 :+: HNil
scala> val m = "p" :+: 3 :+: 't' :+: Some("is") :+: HNil
m: /**/ = p :+: 3 :+: t :+: Some(is) :+: HNil
scala> m.updated(2, 110)
res20: /**/ = p :+: 3 :+: 110 :+: Some(is) :+: HNil
scala> m.updated(0, 2.718)
res21: /**/ = 2.718 :+: 3 :+: t :+: Some(is) :+: HNil
scala> m.updatedRight(0, List("dogs", "cats"))
res22: /**/ = p :+: 3 :+: t :+: List(dogs, cats) :+: HNil
scala> m.updatedRight(2, None)
res24: /**/ = p :+: None :+: t :+: Some(is) :+: HNil
scala> m.remove(0)
res28: /**/ = 3 :+: t :+: Some(is) :+: HNil
scala> m.remove(3)
res29: /**/ = p :+: 3 :+: t :+: HNil
scala> m.removeRight(0)
res30: /**/ = p :+: 3 :+: t :+: HNil
scala> m.removeRight(2)
res31: /**/ = p :+: t :+: Some(is) :+: HNil
scala> m.indexMap(1, (i: Int) => i << 2)
res34: /**/ = p :+: 12 :+: t :+: Some(is) :+: HNil
scala> m.indexMapRight(1, (c: Char) => (c.toInt, c))
res35: /**/ = p :+: 3 :+: (116,t) :+: Some(is) :+: HNil
m.indexFlatMap(2, (c: Char) => c.toInt :+: (c.toString + "asty" :+: HNil))
res38: /**/ = p :+: 3 :+: 116 :+: tasty :+: Some(is) :+: HNil
scala> m.indexFlatMapRight(1, (c: Char) => Some(c.toString + "ense") :+: "Negotiations" :+: 42 :+: HNil)
res39: /**/ = p :+: 3 :+: Some(tense) :+: Negotiations :+: 42 :+: Some(is) :+: HNil
The index is the position at which the inserted element will go.
scala> m.insert(0, 3.14159)
res41: /**/ = 3.14159 :+: p :+: 3 :+: t :+: Some(is) :+: HNil
scala> m.insert(2, "2.718")
res42: /**/ = p :+: 3 :+: 2.718 :+: t :+: Some(is) :+: HNil
scala> m.insertRight(0, Some(6.62607))
res43: /**/ = p :+: 3 :+: t :+: Some(is) :+: Some(6.62607) :+: HNil
scala> m.insertRight(1, None)
res44: /**/ = p :+: 3 :+: t :+: None :+: Some(is) :+: HNil
The index is the position at which the head of the inserted hlist will go.
scala> m.insertM(0, true :+: "foo" :+: HNil)
res45: /**/ = true :+: foo :+: p :+: 3 :+: t :+: Some(is) :+: HNil
scala> m.insertM(2, true :+: "foo" :+: HNil)
res46: /**/ = p :+: 3 :+: true :+: foo :+: t :+: Some(is) :+: HNil
scala> m.insertMRight(0, true :+: "foo" :+: HNil) // essentially concatenation
res47: /**/ = p :+: 3 :+: t :+: Some(is) :+: true :+: foo :+: HNil
scala> m.insertMRight(2, true :+: "foo" :+: HNil)
res48: /**/ = p :+: 3 :+: true :+: foo :+: t :+: Some(is) :+: HNil
scala> val w = 3 :+: true :+: "asdf" :+: 'k' :+: () :+: 9.3 :+: HNil
w: /**/ = 3 :+: true :+: asdf :+: k :+: () :+: 9.3 :+: HNil
scala> w.splitAt(0)
res50: /**/ = (HNil,3 :+: true :+: asdf :+: k :+: () :+: 9.3 :+: HNil)
scala> w.splitAt(3)
res51: /**/ = (3 :+: true :+: asdf :+: HNil,k :+: () :+: 9.3 :+: HNil)
scala> w.splitAtRight(2)
res53: /**/ = (3 :+: true :+: asdf :+: k :+: HNil,() :+: 9.3 :+: HNil)
scala> w.splitAtRight(4)
res54: /**/ = (3 :+: true :+: HNil,asdf :+: k :+: () :+: 9.3 :+: HNil)
Type-indexes support at
, take
, drop
, update
, remove
, map
, flatmap
, element insertion
, hlist insertion
and splitAt
scala> w.t[String].at
res55: String = asdf
scala> w.t[String].before
res56: /**/ = 3 :+: true :+: HNil
scala> w.t[String].after
res57: /**/ = k :+: () :+: 9.3 :+: HNil
scala> w.t[String].drop
res58: /**/ = asdf :+: k :+: () :+: 9.3 :+: HNil
scala> w.t[String].take
res59: /**/ = 3 :+: true :+: HNil
scala> w.t[String].updated(19)
res60: /**/ = 3 :+: true :+: 19 :+: k :+: () :+: 9.3 :+: HNil
scala> w.t[Unit].remove
res61: /**/ = 3 :+: true :+: asdf :+: k :+: 9.3 :+: HNil
scala> w.t[Char].map(_.isUpper)
res62: /**/ = 3 :+: true :+: asdf :+: false :+: () :+: 9.3 :+: HNil
scala> w.t[String].flatMap(s => s(0) :+: s.substring(1) :+: HNil)
res63: /**/ = 3 :+: true :+: a :+: sdf :+: k :+: () :+: 9.3 :+: HNil
scala> w.t[String].insert(Some(4.4))
res64: /**/ = 3 :+: true :+: Some(4.4) :+: asdf :+: k :+: () :+: 9.3 :+: HNil
scala> w.t[String].insertM(Some(true) :+: None :+: HNil)
res65: /**/ = 3 :+: true :+: Some(true) :+: None :+: asdf :+: k :+: () :+: 9.3 :+: HNil
scala> w.t[Char].splitAt
res66: /**/ = (3 :+: true :+: asdf :+: HNil,k :+: () :+: 9.3 :+: HNil)
When indexing by type, if case multiple elements have the indexed type, the element furthest to the right is selected
scala> val ti = 3.14159 :+: "john" :+: List("puppy, kitten") :+: Some("snow") :+: "ramsay" :+: (22L, 11.14) :+: HNil
ti: /**/ = 3.14159 :+: john :+: List(puppy, kitten) :+: Some(snow) :+: ramsay :+: (22,11.14) :+: HNil
scala> ti.t[String].at
res68: String = ramsay
The length of the resulting list is the minimum of the length of the originals
scala> val rz = 3 :+: "ai4" :+: List('r', 'H') :+: HNil; val sz = '3' :+: 2 :+: 'j' :+: "sdfh" :+: HNil
rz: /**/ = 3 :+: ai4 :+: List(r, H) :+: HNil
sz: /**/ = 3 :+: 2 :+: j :+: sdfh :+: HNil
scala> rz zip sz
res69: /**/ = (3,3) :+: (ai4,2) :+: (List(r, H),j) :+: HNil
scala> sz zip rz
res70: /**/ = (3,3) :+: (2,ai4) :+: (j,List(r, H)) :+: HNil
scala> rz zip sz.tail
res71: /**/ = (3,2) :+: (ai4,j) :+: (List(r, H),sdfh) :+: HNil
scala> val tpls = (1, true) :+: (Some("string"), "99 bottles of beer on the wall") :+: ("oogachaka", 42L) :+: HNil
tpls: /**/ = (1,true) :+: (Some(string),99 bottles of beer on the wall) :+: (oogachaka,42) :+: HNil
scala> tpls.unzip
res72: /**/ = (1 :+: Some(string) :+: oogachaka :+: HNil,true :+: 99 bottles of beer on the wall :+: 42 :+: HNil)
scala> val list2Option: List ~> Option = new (List ~> Option) {override def apply[T](x: List[T]) = x.headOption}
list2Option: /**/ = /**/
scala> val hl1 = List(1, 2, 3) :+: List[Boolean]() :+: List("oogachaka", "ho gaya") :+: HNil
hl1: /**/ = List(1, 2, 3) :+: List() :+: List(oogachaka, ho gaya) :+: HNil
scala> hl1 transform list2Option
res73: /**/ = Some(1) :+: None :+: Some(oogachaka) :+: HNil
scala> HNil transform list2Option
res74: /**/ = HNil
scala> val list2Down: Vector ~> Id = new (Vector ~> Id) {override def apply[T](x: Vector[T]) = x(0)}
list2Down: /**/ = /**/
scala> val hl2 = Vector(1, 2, 1) :+: Vector(true) :+: HNil
hl2: /**/ = Vector(1, 2, 1) :+: Vector(true) :+: HNil
scala> hl2 down list2Down
res75: /**/ = 1 :+: true :+: HNil
scala> HNil down list2Down
res76: /**/ = HNil
Or you could yoda-apply it for kicks.
scala> val y1 = 9.75 :+: 'x' :+: HNil
y1: /**/ = 9.75 :+: x :+: HNil
scala> val y2 = -2.125 :+: 'X' :+: HNil
y2: /**/ = -2.125 :+: X :+: HNil
scala> val f = ((x: Double) => x + 5) :+: ((x: Char) => x.isUpper) :+: HNil
f: /**/ = <function1> :+: <function1> :+: HNil
scala> f fapply y1
res77: /**/ = 14.75 :+: false :+: HNil
scala> f fapply y2
res78: /**/ = 2.875 :+: true :+: HNil
scala> y1 yapply f // yoda-application: to the data, apply the function
res79: /**/ = 14.75 :+: false :+: HNil
scala> y2 yapply f
res80: /**/ = 2.875 :+: true :+: HNil
scala> val sz1 = List(1, 2, 3) :+: List(true, false) :+: HNil
sz1: /**/ = List(1, 2, 3) :+: List(true, false) :+: HNil
scala> val sz2 = Vector(1, 2, 3) :+: Vector(true, false) :+: Vector("alpha", "beta", "charlie", "delta") :+: Vector(3.14, 2.718, 6.262) :+: HNil
sz2: /**/ = Vector(1, 2, 3) :+: Vector(true, false) :+: Vector(alpha, beta, charlie, delta) :+: Vector(3.14, 2.718, 6.262) :+: HNil
scala> sz1.azipped
res81: /**/ = List(1 :+: true :+: HNil, 2 :+: false :+: HNil)
scala> sz2.azipped
res82: /**/ = Vector(1 :+: true :+: alpha :+: 3.14 :+: HNil, 2 :+: false :+: beta :+: 2.718 :+: HNil)
scala> sz1.zipwith { case i :+: b :+: _ => (b, i) }
res83: List[(Boolean, Int)] = List((true,1), (false,2))
scala> sz2.zipwith { case i :+: b :+: s :+: d :+: _ => ((i, s), (b, d))}
res84: scala.collection.immutable.Vector[((Int, String), (Boolean, Double))] = Vector(((1,alpha),(true,3.14)), ((2,beta),(false,2.718)))
scala> val fibs: Stream[BigInt] = BigInt(0) #:: BigInt(1) #:: fibs.zip(fibs.tail).map { n => n._1 + n._2}
fibs: Stream[BigInt] = Stream(0, ?)
scala> val nats: Stream[Int] = Stream.from(1)
nats: Stream[Int] = Stream(1, ?)
scala> val pows: Stream[Long] = {def go(n: Int): Stream[Long] = (1L << n) #:: go(n + 1); go(1) }
pows: Stream[Long] = Stream(2, ?)
scala> val fz1 = fibs :+: nats :+: HNil
fz1: /**/ = Stream(0, ?) :+: Stream(1, ?) :+: HNil
scala> val fz2 = fibs :+: nats :+: pows :+: HNil
fz2: /**/ = Stream(0, ?) :+: Stream(1, ?) :+: Stream(2, ?) :+: HNil
scala> val fz3 = fibs :+: nats :+: Stream.empty[String] :+: HNil
fz3: /**/ = Stream(0, ?) :+: Stream(1, ?) :+: Stream() :+: HNil
scala> fz1.azipped
res85: /**/ = Stream(0 :+: 1 :+: HNil, ?)
scala> (res85 take 5).toList
res87: /**/ = List(0 :+: 1 :+: HNil, 1 :+: 2 :+: HNil, 1 :+: 3 :+: HNil, 2 :+: 4 :+: HNil, 3 :+: 5 :+: HNil)
scala> fz2.azipped
res88: /**/ = Stream(0 :+: 1 :+: 2 :+: HNil, ?)
scala> (res88 take 3).toVector
res89: /**/ = Vector(0 :+: 1 :+: 2 :+: HNil, 1 :+: 2 :+: 4 :+: HNil, 1 :+: 3 :+: 8 :+: HNil)
scala> fz3.azipped
res90: /**/ = Stream()
scala> fz1.zipwith { case b :+: i :+: _ => (i, b) }
res91: Stream[(Int, BigInt)] = Stream((1,0), ?)
scala> (res91 take 3).toArray
res92: Array[(Int, BigInt)] = Array((1,0), (2,1), (3,1))
scala> fz2.zipwith { case b :+: i :+: l :+: _ => (i, (b, l)) }
res93: Stream[(Int, (BigInt, Long))] = Stream((1,(0,2)), ?)
scala> (res93 take 3).toList
res94: List[(Int, (BigInt, Long))] = List((1,(0,2)), (2,(1,4)), (3,(1,8)))
scala> fz3.zipwith { case b :+: i :+: s :+: _ => (i, b, s) }
res95: Stream[(Int, BigInt, String)] = Stream()
Provided that there exists an implicit conversion to a common type for each element type of a hlist, you can use foreach
, count
, exists
, forall
and foldleft
as you would with a standard library list.
scala> import language.implicitConversions
import language.implicitConversions
scala> :paste
// Entering paste mode (ctrl-D to finish)
trait Lengthable {
def length: Int
}
object Lengthable {
implicit def int2Lengthable(i: Int): Lengthable = new Lengthable {
override def length = i
}
implicit def string2Lengthable(s: String): Lengthable = new Lengthable {
override def length = s.length
}
implicit def seq2Lengthable[T](s: Seq[T]): Lengthable = new Lengthable {
override def length = s.length
}
implicit def bool2Lengthable(b: Boolean): Lengthable = new Lengthable {
override def length = if (b) 1 else 0
}
implicit def tuple2Lengthable[T, U](t: (T, U))(implicit ev0: T => Lengthable, ev1: U => Lengthable): Lengthable =
new Lengthable {
override def length = t._1.length * t._2.length
}
}
// Exiting paste mode, now interpreting.
defined trait Lengthable
defined object Lengthable
scala> val cvt = 3 :+: "oogachaka" :+: List(1, 2, 3) :+: Vector("cow", "chicken") :+: false :+: (5, "ho gaya") :+: HNil
cvt: /**/ = 3 :+: oogachaka :+: List(1, 2, 3) :+: Vector(cow, chicken) :+: false :+: (5,ho gaya) :+: HNil
scala> var maxl = 0
maxl: Int = 0
scala> cvt.foreach[Lengthable](l => maxl = math.max(maxl, l.length))
scala> maxl
res98: Int = 35
scala> cvt.exists[Lengthable](_.length < 10)
res99: Boolean = true
scala> cvt.forall[Lengthable](_.length >= 0)
res100: Boolean = true
scala> cvt.forall[Lengthable](_.length > 0)
res101: Boolean = false
scala> cvt.count[Lengthable](_.length > 5)
res102: Int = 2
scala> cvt.foldLeft[Int, Lengthable](0)(_ + _.length)
res103: Int = 52
The element type of the list is the least upper bound of the element types of the HList
scala> val hlc1 = 42 :+: 22L :+: 3.14159 :+: 'c' :+: 2.718f :+: HNil
hlc1: /**/ = 42 :+: 22 :+: 3.14159 :+: c :+: 2.718 :+: HNil
scala> hlc1.toList
res105: List[AnyVal] = List(42, 22, 3.14159, c, 2.718)
scala> val hlc2 = List(1, 2, 3) :+: List(true, false) :+: HNil
hlc2: /**/ = List(1, 2, 3) :+: List(true, false) :+: HNil
scala> hlc2.toList
res106: List[List[AnyVal]] = List(List(1, 2, 3), List(true, false))
scala> val hlc3 = Some("foo") :+: Some(Set(1, 2,3)) :+: HNil
hlc3: /**/ = Some(foo) :+: Some(Set(1, 2, 3)) :+: HNil
scala> hlc3.toList
res107: List[Some[Object]] = List(Some(foo), Some(Set(1, 2, 3)))
scala> val hlc4 = Some("foo") :+: Some(Set(1, 2,3)) :+: None :+: HNil
hlc4: /**/ = Some(foo) :+: Some(Set(1, 2, 3)) :+: None :+: HNil
scala> hlc4.toList
res109: List[Option[Object]] = List(Some(foo), Some(Set(1, 2, 3)), None)
Tuples support the same operations as hlists. This is achieved by converting the tuple to an hlist, performing the corresponding operation on the hlist and converting back the result. Therefore the performance of an operation on a tuple will be strictly worse than that of the operation on the corresponding hlist. However, tuples are a part of the standard scala library, will be more interoperable with the rest of your codebase and code that uses tuples is, by virtue of familiarity, easier to parse. So unless profiling shows that an hlist-style operation is the bottleneck (which will almost never be the case), feel free to use the operations listed.
As with hlists, all indexed operations (apply
, drop
, take
etc) are 0-based and can be indexed from the left or the right. The type signatures for the tuples aren’t terribly informative in the examples presented below. For clarity, /**/
represents an omitted type signature.
scala> import typequux._; import TupleOps._
import typequux._
import TupleOps._
scala> 22l :*: ("str", true, 1, Some(3.14))
res0: /**/ = (22,str,true,1,Some(3.14))
scala> 3.14 :*: None :*: res0
res1: /**/ = (3.14,None,22,str,true,1,Some(3.14))
scala> val t1 = (Some(3.14), 1, true, "str"); val t2 = ('3', 2, "sdfg")
t1: /**/ = (Some(3.14),1,true,str)
t2: /**/ = (3,2,sdfg)
scala> val t3 = t1 :**: t2
t3: /**/ = (Some(3.14),1,true,str,3,2,sdfg)
scala> t1.reverse
res3: /**/ = (str,true,1,Some(3.14))
scala> t2.reverse
res4: /**/ = (sdfg,2,3)
scala> t3.reverse
res5: /**/ = (sdfg,2,3,str,true,1,Some(3.14))
scala> t1.reverse.reverse == t1
res10: Boolean = true
scala> t2.reverse.reverse == t2
res11: Boolean = true
scala> t3.reverse.reverse == t3
res12: Boolean = true
scala> t1.length
res13: Long = 4
scala> t2.length
res14: Long = 3
scala> t3.length
res15: Long = 7
scala> val p = (3, true, "asdf", false, 'k', (), 13, 9.3)
p: /**/ = (3,true,asdf,false,k,(),13,9.3)
scala> p(0)
res21: Int = 3
scala> p(3)
res22: Boolean = false
scala> p.right(0)
res23: Double = 9.3
scala> p.right(1)
res25: Int = 13
The argument is the number of elements to drop.
scala> p.drop(3)
res26: /**/ = (false,k,(),13,9.3)
scala> p.dropRight(2)
res27: /**/ = (3,true,asdf,false,k,())
The argument is the number of elements to take.
scala> p.take(2)
res28: /**/ = (3,true)
scala> p.takeRight(4)
res29: /**/ = (k,(),13,9.3)
scala> p.updated(0, None)
res30: /**/ = (None,true,asdf,false,k,(),13,9.3)
scala> p.updated(2, List(true, false))
res31: /**/ = (3,true,List(true, false),false,k,(),13,9.3)
scala> p.updatedRight(0, "oogachaka")
res32: /**/ = (3,true,asdf,false,k,(),13,oogachaka)
scala> p.updatedRight(3, 42)
res33: /**/ = (3,true,asdf,false,42,(),13,9.3)
scala> p.remove(0)
res34: /**/ = (true,asdf,false,k,(),13,9.3)
scala> p.remove(4)
res35: /**/ = (3,true,asdf,false,(),13,9.3)
scala> p.removeRight(0)
res36: /**/ = (3,true,asdf,false,k,(),13)
scala> p.removeRight(2)
res37: /**/ = (3,true,asdf,false,k,13,9.3)
scala> val m = ("p", 3, 't', List("puppies", "kittens", "goat"), Some("is"))
m: /**/ = (p,3,t,List(puppies, kittens, goat),Some(is))
scala> m.indexMap(1, (i: Int) => i << 2)
res42: /**/ = (p,12,t,List(puppies, kittens, goat),Some(is))
scala> m.indexMapRight(2, (c: Char) => c.toInt)
res43: /**/ = (p,3,116,List(puppies, kittens, goat),Some(is))
So long as a tuple of the resulting arity can be constructed.
scala> m.indexFlatMap(2, (c: Char) => (c, c.toInt, c.toString + "ype quux"))
res44: /**/ = (p,3,t,116,type quux,List(puppies, kittens, goat),Some(is))
scala> m.indexFlatMapRight(1, (l: List[String]) => (l.length, l.filter(_.length > 4), l.map(s => Some(s))))
res50: /**/ = (p,3,t,3,List(puppies, kittens),List(Some(puppies), Some(kittens), Some(goat)),Some(is))
The index is the position at which the inserted element will go.
scala> m.insert(0, None)
res52: /**/ = (None,p,3,t,List(puppies, kittens, goat),Some(is))
scala> m.insert(2, Some(42))
res53: /**/ = (p,3,Some(42),t,List(puppies, kittens, goat),Some(is))
scala> m.insertRight(0, None)
res54: /**/ = (p,3,t,List(puppies, kittens, goat),Some(is),None)
scala> m.insertRight(2, List(0, 1, 1, 2, 3, 5, 8))
res55: /**/ = (p,3,t,List(0, 1, 1, 2, 3, 5, 8),List(puppies, kittens, goat),Some(is))
The index is the position at which the first element of the inserted tuple will go.
scala> m.insertM(0, (true, "foo", 2))
res56: /**/ = (true,foo,2,p,3,t,List(puppies, kittens, goat),Some(is))
scala> m.insertM(2, (true, "foo", 2))
res57: /**/ = (p,3,true,foo,2,t,List(puppies, kittens, goat),Some(is))
scala> m.insertMRight(0, (true, "foo", 2))
res58: /**/ = (p,3,t,List(puppies, kittens, goat),Some(is),true,foo,2)
scala> m.insertMRight(1, (true, "foo", 2))
res59: /**/ = (p,3,t,List(puppies, kittens, goat),true,foo,2,Some(is))
scala> m.splitAt(2)
res61: /**/ = ((p,3),(t,List(puppies, kittens, goat),Some(is)))
scala> m.splitAtRight(4)
res62: /**/ = (p,(3,t,List(puppies, kittens, goat),Some(is)))
The arity of the resulting tuple is the minimum of the arity of the originals
scala> val t1 = (true, 1, Some("ho gaya"))
t1: /**/ = (true,1,Some(ho gaya))
scala> val t2 = (99, 3.14, None, List(5, 4, 3, 2, 1))
t2: /**/ = (99,3.14,None,List(5, 4, 3, 2, 1))
scala> t1 zip t2
res64: /**/ = ((true,99),(1,3.14),(Some(ho gaya),None))
scala> t2 zip t1
res65: /**/ = ((99,true),(3.14,1),(None,Some(ho gaya)))
scala> t1 zip t2.drop(1)
res66: /**/ = ((true,3.14),(1,None),(Some(ho gaya),List(5, 4, 3, 2, 1)))
Tuple2
, you can unzip it scala> val tz = (("stannis", 99), (Some("the mannis"), 'c'), (List("husky", "great dane"), Set(1, 2, 3, 4, 4)))
tz: /**/ = ((stannis,99),(Some(the mannis),c),(List(husky, great dane),Set(1, 2, 3, 4)))
scala> tz.unzip
res67: /**/ = ((stannis,Some(the mannis),List(husky, great dane)),(99,c,Set(1, 2, 3, 4)))
scala> val trt = (List(1, 2, 3), List(true, false), List("omega", "alpha"))
trt: /**/ = (List(1, 2, 3),List(true, false),List(omega, alpha))
scala> val sqto: Seq ~> Option = new (Seq ~> Option) {override def apply[T](x: Seq[T]) = x.headOption}
sqto: /**/ = 1@6ded41b4
scala> trt transform sqto
res68: /**/ = (Some(1),Some(true),Some(omega))
scala> import typequux._ // for the Id type constructor
import typequux._
scala> val sqdo: Seq ~> Id = new (Seq ~> Id) {override def apply[T](x: Seq[T]) = x(0)}
sqdo: typequux.~>[Seq,typequux.typequux.Id] = 1@5112b55
scala> trt down sqdo
res69: (Int, Boolean, String) = (1,true,omega)
Or you could yoda-apply it.
scala> val funcs = ((i: Int) => i << 1, (s: String) => s.toUpperCase, (d: Double) => d * 2, (l: List[Int]) => l.headOption)
funcs: (Int => Int, String => String, Double => Double, List[Int] => Option[Int]) = (<function1>,<function1>,<function1>,<function1>)
scala> val args1 = (1, "alpha", 4.0, List(1, 2, 3))
args1: /**/ = (1,alpha,4.0,List(1, 2, 3))
scala> val args2 = (2, "OMEGA", 9.0, List[Int]())
args2: /**/ = (2,OMEGA,9.0,List())
scala> funcs fapply args1
res70: /**/ = (2,ALPHA,8.0,Some(1))
scala> funcs fapply args2
res71: /**/ = (4,OMEGA,18.0,None)
scala> args1 yapply funcs // yoda apply: to this data, apply the function
res72: /**/ = (2,ALPHA,8.0,Some(1))
scala> args2 yapply funcs
res74: /**/ = (4,OMEGA,18.0,None)
scala> val tz1 = (List(1, 2, 3), List(true, false))
tz1: /**/ = (List(1, 2, 3),List(true, false))
scala> val tz2 = (Vector(1, 2, 3), Vector(true, false), Vector("alpha", "beta", "charlie", "delta"), Vector(3.14, 2.718, 6.262))
tz2: /**/ = (Vector(1, 2, 3),Vector(true, false),Vector(alpha, beta, charlie, delta),Vector(3.14, 2.718, 6.262))
scala> tz1.azipped
res75: List[(Int, Boolean)] = List((1,true), (2,false))
scala> tz2.azipped
res76: scala.collection.immutable.Vector[(Int, Boolean, String, Double)] = Vector((1,true,alpha,3.14), (2,false,beta,2.718))
scala> tz1.zipwith { case (i, b) => (b, i) }
res77: List[(Boolean, Int)] = List((true,1), (false,2))
scala> tz2.zipwith { case (i, b, s, d) => ((i, s), (b, d)) }
res78: scala.collection.immutable.Vector[((Int, String), (Boolean, Double))] = Vector(((1,alpha),(true,3.14)), ((2,beta),(false,2.718)))
scala> val fibs: Stream[BigInt] = { def go(i: BigInt, j: BigInt): Stream[BigInt] = (i + j) #:: go(j, i + j); BigInt(0) #:: BigInt(1) #:: go(0, 1)}
fibs: Stream[BigInt] = Stream(0, ?)
scala> val nats: Stream[Int] = Stream.from(1)
nats: Stream[Int] = Stream(1, ?)
scala> val pows: Stream[Long] = { def go(n: Int): Stream[Long] = (1L << n) #:: go(n + 1); go(1)}
pows: Stream[Long] = Stream(2, ?)
scala> val sz1 = (fibs, nats)
sz1: (Stream[BigInt], Stream[Int]) = (Stream(0, ?),Stream(1, ?))
scala> val sz2 = (fibs, nats, pows)
sz2: /**/ = (Stream(0, ?),Stream(1, ?),Stream(2, ?))
scala> val sz3 = (fibs, nats, Stream.empty[String])
sz3: /**/ = (Stream(0, ?),Stream(1, ?),Stream())
scala> sz1.azipped
res79: Stream[(BigInt, Int)] = Stream((0,1), ?)
scala> sz2.azipped
res80: Stream[(BigInt, Int, Long)] = Stream((0,1,2), ?)
scala> sz3.azipped
res81: Stream[(BigInt, Int, String)] = Stream()
scala> (res79 take 3).toVector
res82: Vector[(BigInt, Int)] = Vector((0,1), (1,2), (1,3))
scala> (res80 take 3).toList
res83: List[(BigInt, Int, Long)] = List((0,1,2), (1,2,4), (1,3,8))
scala> sz1.zipwith { case (b, i) => (i, b) }
res84: Stream[(Int, BigInt)] = Stream((1,0), ?)
scala> (res84 take 3).toList
res85: List[(Int, BigInt)] = List((1,0), (2,1), (3,1))
scala> sz2.zipwith { case (b, i, l) => (i, (b, l)) }
res86: Stream[(Int, (BigInt, Long))] = Stream((1,(0,2)), ?)
scala> (res86 take 4).toArray
res87: Array[(Int, (BigInt, Long))] = Array((1,(0,2)), (2,(1,4)), (3,(1,8)), (4,(2,16)))
scala> sz3.zipwith { case (b, i, s) => (i, b, s) }
res88: Stream[(Int, BigInt, String)] = Stream()
Provided that there exists an implicit conversion to a common type for each element of a tuple, you can foreach
, count
, exists
, forall
and foldleft
as you would use with a standard library collection.
scala> import language.implicitConversions
import language.implicitConversions
scala> :paste
// Entering paste mode (ctrl-D to finish)
trait Lengthable {
def length: Int
}
object Lengthable {
implicit def int2Lengthable(i: Int): Lengthable = new Lengthable {
override def length = i
}
implicit def string2Lengthable(s: String): Lengthable = new Lengthable {
override def length = s.length
}
implicit def seq2Lengthable[T](s: Seq[T]): Lengthable = new Lengthable {
override def length = s.length
}
implicit def bool2Lengthable(b: Boolean): Lengthable = new Lengthable {
override def length = if (b) 1 else 0
}
implicit def tuple2Lengthable[T, U](t: (T, U))(implicit ev0: T => Lengthable, ev1: U => Lengthable): Lengthable =
new Lengthable {
override def length = t._1.length * t._2.length
}
}
// Exiting paste mode, now interpreting.
defined trait Lengthable
defined object Lengthable
scala> val cvt = (3, "oogachaka", List(1, 2, 3), Vector("cow", "chicken"), false, (5, "ho gaya"))
cvt: /**/ = (3,oogachaka,List(1, 2, 3),Vector(cow, chicken),false,(5,ho gaya))
scala> var maxl = 0
maxl: Int = 0
scala> cvt.foreach[Lengthable](l => maxl = math.max(maxl, l.length))
scala> maxl
res90: Int = 35
scala> cvt.exists[Lengthable](_.length < 10)
res91: Boolean = true
scala> cvt.forall[Lengthable](_.length > 0)
res92: Boolean = false
scala> cvt.count[Lengthable](_.length > 5)
res93: Int = 2
scala> cvt.foldLeft[Int, Lengthable](0)(_ + _.length)
res94: Int = 52
The element type of the list is the least upper bound of the element types of the tuple.
scala> val tp1 = (42, 22L, 3.14159, 'c', 2.718f)
tp1: (Int, Long, Double, Char, Float) = (42,22,3.14159,c,2.718)
scala> tp1.toList
res95: List[AnyVal] = List(42, 22, 3.14159, c, 2.718)
scala> val tp2 = (List(1, 2,3), List(true, false))
tp2: (List[Int], List[Boolean]) = (List(1, 2, 3),List(true, false))
scala> tp2.toList
res97: List[List[AnyVal]] = List(List(1, 2, 3), List(true, false))
scala> val tp3 = (Some("foo"), Some(Set(1,2 , 3)))
tp3: (Some[String], Some[scala.collection.immutable.Set[Int]]) = (Some(foo),Some(Set(1, 2, 3)))
scala> tp3.toList
res98: List[Some[Object]] = List(Some(foo), Some(Set(1, 2, 3)))
scala> val tp4 = (Some("foo"), Some(Set(1,2 , 3)), None)
tp4: (Some[String], Some[scala.collection.immutable.Set[Int]], None.type) = (Some(foo),Some(Set(1, 2, 3)),None)
scala> tp4.toList
res99: List[Option[Object]] = List(Some(foo), Some(Set(1, 2, 3)), None)
SizedVectors are vectors with statically known sizes. They differ from StringIndexedCollections in that they are sequentially indexed by an integer rather than by a string. Therefore, they are like tuples in which each element has the same type.
The indices are 0-based and indexation can begin on the left or the right. The type signatures for SizedVectors can be quite
complicated and are not terribly informative. For clarity, they are replaced below by \**\
.
The common feature of the supported operations is that the size information is preserved. Supported operations are:
scala> import typequux._ // package
import typequux._
scala> import Typequux._ // useful imports
import Typequux._
scala> val sz1 = SizedVector(1, 2, 3)
sz1: /**/ = SizedVector(1, 2, 3)
scala> val sz2 = SizedVector(true, false, false, true, true)
sz2: /**/ = SizedVector(true, false, false, true, true)
scala> def testCons[N <: Dense, T](a: SizedVector[N, T], b: SizedVector[N, T]): Boolean = true
testCons: [N <: typequux.Dense, T](a: typequux.SizedVector[N,T], b: typequux.SizedVector[N,T])Boolean
scala> testCons(SizedVector(1.1, 2.2, 3.3), SizedVector(3.14, 2.718, 6.626))
res0: Boolean = true
scala> testCons(SizedVector(1.1, 2.2, 3.3), SizedVector(3.14, 2.718, 6.626, 1.618)) // does not compile
scala> val sz3 = SizedVector.from(2, List("fry", "bender"))
sz3: Option[/**/] = Some(SizedVector(fry, bender))
scala> val sz4 = SizedVector.from(2, List("fry", "bender", "amy")) // static guarentee on runtime size
sz4: Option[/**/] = None
scala> val sz5 = SizedVector("fry", "bender", "amy")
sz5: /**/ = SizedVector(fry, bender, amy)
scala> val sz6 = SizedVector("professor", "zoidberg")
sz6: /**/ = SizedVector(professor, zoidberg)
scala> val sz7 = SizedVector(1.1, 2.2, 3.3)
sz7: /**/ = SizedVector(1.1, 2.2, 3.3)
scala> 100.0 +: sz7 // prepend
res5: /**/ = SizedVector(100.0, 1.1, 2.2, 3.3)
scala> sz7 :+ 100.0 // append
res6: /**/ = SizedVector(1.1, 2.2, 3.3, 100.0)
scala> val sz8 = sz5 ++ sz6 // concatenation
sz8: /**/ = SizedVector(fry, bender, amy, professor, zoidberg)
scala> sz5
res9: /**/ = SizedVector(fry, bender, amy)
scala> sz5(0)
res10: String = fry
scala> sz5(2)
res11: String = amy
scala> sz5(4) // does not compile
<console>:19: error: Cannot prove that typequux.Bool.True.type =:= typequux.Bool.False.type.
sz5(4)
scala> sz8.updated(2, "zapp brannigan")
res15: /**/ = SizedVector(fry, bender, zapp brannigan, professor, zoidberg)
scala> sz8.updated(0, "richard nixon")
res17: /**/ = SizedVector(richard nixon, bender, amy, professor, zoidberg)
scala> sz8.updated(12, "richard nixon") // does not compile
scala> sz5.length
res18: Int = 3
scala> sz8.length
res19: Int = 5
scala> sz5.reverse
res20: /**/ = SizedVector(amy, bender, fry)
scala> sz8.reverse
res21: /**/ = SizedVector(zoidberg, professor, amy, bender, fry)
scala> sz8.reverse.reverse == sz8
res24: Boolean = true
scala> sz8.drop(2)
res26: /**/ = SizedVector(amy, professor, zoidberg)
scala> sz8.dropRight(3)
res27: /**/ = SizedVector(fry, bender)
scala> sz8.drop(20) // does not compile
scala> sz8.dropRight(20) // does not compile
scala> sz8.take(2)
res30: /**/ = SizedVector(fry, bender)
scala> sz8.takeRight(2)
res31: /**/ = SizedVector(professor, zoidberg)
scala> sz8.take(11) // does not compile
scala> sz8.takeRight(11) // does not compile
scala> sz8.slice(0, 3)
res35: /**/ = SizedVector(fry, bender, amy)
scala> sz8.slice(2, 4)
res36: /**/ = SizedVector(amy, professor)
scala> sz8.slice(1, 10) // does not compile
scala> sz8 map (s => s.length)
res40: /**/ = SizedVector(3, 6, 3, 9, 8)
scala> sz8 map (s => s.toUpperCase)
res41: /**/ = SizedVector(FRY, BENDER, AMY, PROFESSOR, ZOIDBERG)
scala> sz8.sortBy(_.length)
res42: /**/ = SizedVector(fry, amy, bender, zoidberg, professor)
scala> sz8.sortWith(_.length > _.length)
res43: /**/ = SizedVector(professor, zoidberg, bender, fry, amy)
scala> sz8.sorted
res44: /**/ = SizedVector(amy, bender, fry, professor, zoidberg)
scala> val v = SizedVector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
v: /**/ = SizedVector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
scala> v.splitAt(2)
res45: /**/ = (SizedVector(1, 2),SizedVector(3, 4, 5, 6, 7, 8, 9, 10))
scala> v.splitAt(8)
res46: /**/ = (SizedVector(1, 2, 3, 4, 5, 6, 7, 8),SizedVector(9, 10))
scala> v.splitAt(20) // does not compile
If the element type is itself a SizedVector
of type T
, you can flatten the original to obtain a SizedVector
of type T
.
scala> :paste
// Entering paste mode (ctrl-D to finish)
val el11 = SizedVector(1, 2, 3)
val el12 = SizedVector(4, 5, 6)
val el13 = SizedVector(7, 8, 9)
val el14 = SizedVector(10, 11, 12)
// Exiting paste mode, now interpreting.
el11: /**/ = SizedVector(1, 2, 3)
el12: /**/ = SizedVector(4, 5, 6)
el13: /**/ = SizedVector(7, 8, 9)
el14: /**/ = SizedVector(10, 11, 12)
scala> val el1 = SizedVector(el11, el12, el13, el14)
el1: /**/ = SizedVector(SizedVector(1, 2, 3), SizedVector(4, 5, 6), SizedVector(7, 8, 9), SizedVector(10, 11, 12))
scala> el1.flatten
res48: /**/ = SizedVector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
scala> v.flatten // does not compile
scala> val zp = sz8 zip v
zp: /**/ = SizedVector((fry,1), (bender,2), (amy,3), (professor,4), (zoidberg,5))
scala> zp.unzip
res50: /**/
scala> res50._1
res51: /**/ = SizedVector(fry, bender, amy, professor, zoidberg)
scala> res50._2
res52: /**/ = SizedVector(1, 2, 3, 4, 5)
This can help interop with the rest of your codebase.
scala> sz8.backing
res53: Vector[String] = Vector(fry, bender, amy, professor, zoidberg)
scala> v.backing
res54: Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
String indexed collections are similar to immutable associative maps between string keys and values, all of which have to be
of the same type. They differ from standard collections like Map
in that the presence or absence of a key can be
guarenteed at compile time.
String Indexed collections use a TypeMap to store the keys and their association
and scala Vectors
to store the values. The type signatures for StringIndexedCollections can be quite complicated and
are not terribly informative. For clarity, they are replaced below by \**\
. Supported operations are:
scala> import typequux._ // package
import typequux._
scala> import Typequux._ // useful imports
import Typequux._
scala> val pl1 = SINil.add("goku", 32000).add("piccolo", 3500).add("krillin", 1770)
pl1: /**/
scala> val pl2 = pl1.add("tien", 1830)
pl2: /**/
If a key exists, the construction will fail at compile time. Only keys that are not present can be added.
scala> val pl3 = pl1.add("goku", 9000) // does not compile
scala> pl1("goku")
res0: Int = 32000
scala> List(pl2("krillin"), pl2("tien"))
res1: List[Int] = List(1770, 1830)
scala> pl1("gohan") // does not compile
Only keys which are present can be updated
scala> val pl4 = pl1.updated("goku", 150000000)
pl4: /**/
scala> pl1("goku")
res4: Int = 32000
scala> pl4("goku")
res5: Int = 150000000
scala> pl1.updated("yamcha", 12) // does not compile
scala> pl1.size
res7: Int = 3
scala> pl2.size
res8: Int = 4
scala> pl4.size
res9: Int = 3
This can help interop with the rest of your library.
scala> pl1.toMap
res10: Map[String,Int] = Map(goku -> 32000, piccolo -> 3500, krillin -> 1770)
scala> pl2.toMap
res11: Map[String,Int] = Map(goku -> 32000, piccolo -> 3500, krillin -> 1770, tien -> 1830)
scala> pl4.toMap
res12: Map[String,Int] = Map(goku -> 150000000, piccolo -> 3500, krillin -> 1770)
Records are similar to immutable associative maps between strings and values except in two ways:
Therefore, in a way, they are like ad-hoc classes. Records use a TypeMap to store the
keys and their association and a HList to store the values.
The type signatures for records can be quite complicated and are not terribly informative. For clarity, they are replaced below by /**/
.
Supported operations are:
scala> import typequux._ // package
import typequux._
scala> import Typequux._ // useful imports
import Typequux._
scala> val r1 = RNil.add("name", "goku").add("powerlevel", 8000).add("friends", List("krillin", "yamcha", "bulma"))
r1: /**/
scala> val r2 = r1.add("family", List("gohan", "chichi")) // vegeta saga
r2: /**/
If a key exists, construction will fail at compile time. Only keys that are not present can be added.
scala> val r3 = r1.add("powerlevel", 9000) // does not compile
Note that the specific types are preserved
scala> r1("name")
res0: String = goku
scala> r2("powerlevel")
res1: Int = 8000
scala> r2("family")
res2: List[String] = List(gohan, chichi)
scala> r1("family") // does not compile
Only keys that are present can be updated
scala> val r4 = r1.updated("powerlevel", 32000) // kaioken attack
r4: /**/
scala> r1("powerlevel")
res4: Int = 8000
scala> r4("powerlevel")
res5: Int = 32000
scala> r1.updated("teacher", "king kai") // does not compile
scala> r1.size
res7: Int = 3
scala> r2.size
res8: Int = 4
scala> r4.size
res10: Int = 3
This can help interop with the rest of your library. The type of the value of the map is the least upper bound of the types of the elements of the record.
scala> val r5 = RNil.add("a", 1).add("b", 4L).add("c", 'c')
r5: /**/
scala> r5.toMap
res11: Map[String,AnyVal] = Map(c -> c, b -> 4, a -> 1)
scala> val r6 = RNil.add("a", List(1, 2, 3)).add("b", Set(true, false)).add("c", Vector(1.1, 2.2))
r6: /**/
scala> r6.toMap
res12: Map[String,scala.collection.immutable.Iterable[AnyVal] with Int with Boolean => AnyVal] = Map(c -> Vector(1.1, 2.2), b -> Set(true, false), a -> List(1, 2, 3))
scala> val r7 = RNil.add("a", Some((1, 2,3))).add("b", None)
r7: /**/
scala> r7.toMap
res13: Map[String,Option[(Int, Int, Int)]] = Map(b -> None, a -> Some((1,2,3)))
Records can be built from classes. In this case, the public values, getters and case accessors are converted to keys in the record.
scala> case class Foo(i: Int, d: Double, s: String)
defined class Foo
scala> val rc1 = Record.class2Record(Foo(1, 3.14159, "oogachaka"))
rc1: /**/
scala> rc1.toMap
res14: Map[String,Any] = Map(i -> 1, d -> 3.14159, s -> oogachaka)
scala> class Bar(val i: List[Int], val d: List[Double], s: String)
defined class Bar
scala> val rc2 = Record.class2Record(new Bar(List(1, 2,3), List(3.14159, 2.718, 6.626), "alpha"))
rc2: /**/
scala> rc2.toMap
res15: Map[String,List[AnyVal]] = Map(i -> List(1, 2, 3), d -> List(3.14159, 2.718, 6.626))
scala> class Baz(val v: Int){var unsafeBool = true}
defined class Baz
scala> val rc3 = Record.class2Record(new Baz(42))
rc3: /**/
scala> rc3.toMap
res16: Map[String,AnyVal] = Map(v -> 42, unsafeBool -> true)
scala> class Quux(val i: Int){def rnd: Boolean = util.Random.nextBoolean}
defined class Quux
scala> val rc4 = Record.class2Record(new Quux(42))
rc4: /**/
scala> rc4.toMap
res17: Map[String,Int] = Map(i -> 42)
Constraints are composable typeclasses that abstract over the specifics of primitives likes HLists and Records to encode an invariant associated with a problem. They are the central conceptual abstraction of operations on the primitives that ship with TypeQuux and play the major role in supporting the claim that the library is hackable.
As an example, consider the TakeConstraint
:
trait TakeConstraint[N, HL, R] {
def apply(hl: HL): R
}
The invariant that it encodes is that given an object of type HL
, the type of the first N
elements is R
. The semantics are of course
enforced by convention, but are consistent in the default implementations for TakeConstraint
that ship with the library.
If you leave all three type parameters unconstrained, the code will compile for all types for which such a constraint can be satisfied, regardless of arity or the specific primitive.
scala> import typequux._; import Typequux._; import TupleOps._; import constraint._
import typequux._
import Typequux._
import TupleOps._
import constraint._
scala> def uncons[T, R](i: LiteralHash[Int], t: T)(implicit ev: TakeConstraint[i.ValueHash, T, R]): R = ev(t)
uncons: [T, R](i: typequux.LiteralHash[Int], t: T)(implicit ev: typequux.constraint.TakeConstraint[i.ValueHash,T,R])R
scala> uncons(2, (true, "foo", 42)) // tuple
res0: (Boolean, String) = (true,foo)
scala> uncons(2, (true, "foo", List(3.14159, 2.71828), 42L, Some("bananas"))) // tuple
res1: (Boolean, String) = (true,foo)
scala> uncons(2, Some("bananas") :+: "watermelon" :+: 0 :+: HNil) // HList
res3: /**/ = Some(bananas) :+: watermelon :+: HNil
scala> uncons(4, (12, "22/7")) // does not compile
If you fix type T
, it constrains the indices for which the expression compiles and also the primitives that are valid.
scala> def constr1[R](i: LiteralHash[Int], t: (Int, String, Boolean))(implicit ev: TakeConstraint[i.ValueHash, (Int, String, Boolean), R]): R = ev(t)
constr1: [R](i: typequux.LiteralHash[Int], t: (Int, String, Boolean))(implicit ev: typequux.constraint.TakeConstraint[i.ValueHash,(Int, String, Boolean),R])R
scala> constr1(1, (42, "foo", true))
res6: Int = 42
scala> constr1(2, (42, "foo", true))
res7: (Int, String) = (42,foo)
scala> constr1(4, (42, "foo", true)) // does not compile
If you fix type R
, it constrains the shape of the input and also constrains the index to a singular value.
scala> def constr2[T](i: LiteralHash[Int], t: T)(implicit ev: TakeConstraint[i.ValueHash, T, (Int, Boolean)]): (Int, Boolean) = ev(t)
constr2: [T](i: typequux.LiteralHash[Int], t: T)(implicit ev: typequux.constraint.TakeConstraint[i.ValueHash,T,(Int, Boolean)])(Int, Boolean)
scala> constr2(2, (42, true, List(1, 2,3)))
res10: (Int, Boolean) = (42,true)
scala> constr2(2, (42, true, Some(List(true, false)), "oogachaka", ('t', 'q')))
res11: (Int, Boolean) = (42,true)
scala> constr2(3, (42, true, Some(List(true, false)), "oogachaka", ('t', 'q'))) // does not compile
scala> constr2(2, (42)) // does not compile
Constraints are composable. They can be combined with other constraints to encode very granular problem requirements. (This is infact how most of the library is implemented). Consider the following (very hypothetical) scenario:
You have to take the first k elements of a primitive (arity and type is unconstrained), but they have to be convertable to a
list of Option[AnyRef]
. In the real world, the least upper bound type would be some sort of a business object, but
to keep things simple, lets say that its an AnyRef
.
scala> def constr3[T, R](i: LiteralHash[Int], t: T)(implicit ev0: TakeConstraint[i.ValueHash, T, R], ev1: ToListConstraint[R, Option[AnyRef]]): List[Option[AnyRef]] = ev1(ev0(t))
constr3: [T, R](i: typequux.LiteralHash[Int], t: T)(implicit ev0: typequux.constraint.TakeConstraint[i.ValueHash,T,R], implicit ev1: typequux.constraint.ToListConstraint[R,Option[AnyRef]])List[Option[AnyRef]]
scala> constr3(2, (Some("oogachaka"), None, Some(List(1,2 ,3))))
res24: List[Option[Object]] = List(Some(oogachaka), None)
scala> constr3(3, (None, Some(Set(true, false, false)), Some("oogachaka"), None, Some(List(1,2 ,3))))
res25: List[Option[Object]] = List(None, Some(Set(true, false)), Some(oogachaka))
scala> constr3(3, None :+: Some("matt le blanc") :+: Some(List("trevor noah", "john oliver")) :+: None :+: HNil)
res27: List[Option[Object]] = List(None, Some(matt le blanc), Some(List(trevor noah, john oliver)))
scala> constr3(2, Some(12) :+: Some("matt le blanc") :+: Some(List("jon stewart", "jay leno")) :+: None :+: HNil) // does not compile, Int is not a subtype of AnyRef
scala> constr3(5, (Some("foo"), None)) // does not compile, cannot take
If this is a constraint that is recurrent in several parts of your program or you wish to make the code easier to read, you can refactor it like so:
scala> :paste
// Entering paste mode (ctrl-D to finish)
trait MyConstraint[N, R]{
def apply(r: R): List[AnyRef]
}
object MyConstraint{
implicit def build[N, R, T](
implicit ev0: TakeConstraint[N, R, T], ev1: ToListConstraint[T, AnyRef]): MyConstraint[N, R] =
new MyConstraint[N, R]{
override def apply(r: R) = ev1(ev0(r))
}
}
// Exiting paste mode, now interpreting.
defined trait MyConstraint
defined object MyConstraint
scala> def constr4[R](i: LiteralHash[Int], r: R)(implicit ev: MyConstraint[i.ValueHash, R]): List[AnyRef] = ev(r)
constr4: [R](i: typequux.LiteralHash[Int], r: R)(implicit ev: MyConstraint[i.ValueHash,R])List[AnyRef]
scala> constr4(2, (Some("oogachaka"), None, Some(List(1,2 ,3))))
res32: List[Object] = List(Some(oogachaka), None)
scala> constr4(3, (None, Some(Set(true, false, false)), Some("oogachaka"), None, Some(List(1,2 ,3))))
res33: List[Object] = List(None, Some(Set(true, false)), Some(oogachaka))
scala> constr4(3, None :+: Some("matt le blanc") :+: Some(List("trevor noah", "john oliver")) :+: None :+: HNil)
res34: List[Object] = List(None, Some(matt le blanc), Some(List(trevor noah, john oliver)))
scala> constr4(5, (Some("foo"), None)) // does not compile
scala> constr4(2, 'c' :+: Some("matt le blanc") :+: Some(List("jon stewart", "jay leno")) :+: None :+: HNil) // does not compile
There are 40 constraints that ship with the current version of the library. You can check them out in the API.