I like physics. The reason behind it is that physics has a beautiful and consistent type system. Let’s start with very simple scalar model, that is used in Newton mechanics. What we need here are several types like Time, Acceleration, Velocity, Displacement for kinematics. If we also deal with dynamic, we need Mass, Force, Momentum and Energy. We are also introducing Constant, that is just a number and can be used solely as multiplier (or divider, but not implemented).

You can run the whole example here.

Let’s continue with our model:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
object types {
  import scala.annotation.targetName
  opaque type DoubleBased = Double // root type
  type Constructor[T] = scala.Conversion[Double, T] // constructor plus conversion for free

  extension[T <: DoubleBased] (x: T) {
    def asDouble: Double = x
  }
  extension[T<: DoubleBased] (x: T)(using b: Constructor[T]) { // notation with explicit usage of using 
    def +(y: T): T = b(x + y)
  }
  // Constant
  opaque type Constant <: DoubleBased = Double
  given Constant: Constructor[Constant] = (p:Double) => p

  extension[T <: DoubleBased : Constructor] (x: Constant) { // alternative notation (not using used) 
    @targetName("Constant*")
    def *(y: T): T = summon[Constructor[T]](x * y) // summon is used to obtain Constructor
  }
  // TIME
  opaque type Time <: DoubleBased = Double
  given Time: Constructor[Time] = (p:Double) => p
  //ACCELERATION
  opaque type Acceleration <: DoubleBased = Double
  given Acceleration:Constructor[Acceleration] = (p:Double) => p

  extension (x: Acceleration) {
    @targetName("Acceleration*")
    def *(y: Time): Velocity = x * y
  }
  // VELOCITY 
  opaque type Velocity <: DoubleBased = Double
  given Velocity: Constructor[Velocity] = (p:Double) => p

  extension (x: Velocity) {
    @targetName("Velocity*")
    def *(y: Time): Displacement = x * y
  }
  // DISPLACEMENT 
  opaque type Displacement <: DoubleBased = Double
  given Displacement:Constructor[Displacement] = (p:Double) => p
  // MASS 
  opaque type Mass <: DoubleBased = Double
  given Mass:Constructor[Mass] = (p:Double) => p

  extension(x: Mass) {
    @targetName("Mass1*")
    def * (y:Velocity):Momentum = x * y
    @targetName("Mass2*")
    def * (y:Acceleration):Force= x * y
  }
  // FORCE 
  opaque type Force <: DoubleBased = Double
  given Force:Constructor[Force] = (p:Double) => p

  extension(x: Force) {
    @targetName("Force*")
    def * (y:Time):Momentum = x * y
  }
  // MOMENTUM 
  opaque type Momentum <: DoubleBased = Double
  given Momentum:Constructor[Momentum] = (p:Double) => p

  extension(x: Momentum) {
    @targetName("Momentum*")
    def * (y:Velocity):Energy = x * y
  }
  // ENERGY 
  opaque type Energy <: DoubleBased = Double
  given Energy:Constructor[Energy] = (p:Double) => p
}

Only multiplication and sum operators are defined in this example, but extension with / or - is pretty straightforward. We introduced root type named DoubleBased, the reason for it is, that we want to have any subtype convertible to the Double (see asDouble extension ) and also what is more important, we want to have ability to use sum on any defined type (see + extension). asDouble extension is trivial, but following + extension is more complicated, because it uses Constructor as implicit parameter. All constructors are declared with given keyword. It means, that givens can be injected implicitly. See (using b: Constructor[T]) or summon[Constructor[T]] in the above code snippet. Importantly, the constructor function is also declared as conversion function (scala.Conversion). More details about implicit conversions can be found here. The consequence is, that if import scala.language.implicitConversions than constructor functions work also as implicit conversions.

Let’s spend some time with Constant: * extension. Here we have the same situation, where we need to use constructor. The reason for it is, that we want to write formulas like Constant(10)*v (remark: const * T ~ T , meaning that type is conserved).

Let’s construct some formulas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package formulas {
  import types.{given, _}
  import scala.language.implicitConversions
  
  def velocity(u:Velocity, a:Acceleration, t:Time): Velocity = 
  	u + a*t 
  def displacement(u:Velocity, a:Acceleration, t:Time): Displacement = 
  	u*t + Constant(0.5)*a*t*t
  def energy(e0:Energy, m:Mass, v:Velocity):Energy =
  	e0 + 0.5*m*v*v
  def energy(using e0:Energy, m:Mass, a:Acceleration, t:Time):Energy = 
  	e0 + 0.5*m*a*t*(a*t) // fomula need help with brackets 
}

Our first formula (velocity) is the most simple one. The result of a*t is velocity. In this case operator * is from Acceleration extension. Than the + operator calculates the sum of two velocities. In second formula is Constant constructed with constructor explicitly, in other two is implicit conversion used.

Let’s put everything together into a running example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
object Main {
  def main(args: Array[String]): Unit = {
    import types.{given, _}
    import formulas._
    import scala.language.implicitConversions // try comment

    val v1 = Velocity(13.0) // direct declaration 
    val v2 = velocity(10.0, 10.0, 1.0) + v1 // sum, velocity constructed with implicit conversion
    val v3 =  v1 + (42 - 13) // implicit conversion
    println(s"v1 = $v1, v2= $v2, v3 = $v3")
    
    //val v4 =  13.5 + v3 // not compilable
       
    val e = energy(0.0, 20.0, math.sqrt(4.2)) // formula with speed, implicit conversion
    println(s"e = ${e}")
    
    given tg:Time = Time(1)
    given mg:Mass = Mass(2 * 41)
    given ag:Acceleration = Acceleration(1)
    given e0g:Energy = Energy(1)
    
    val e1 = energy // attributes provided implicitly 
    println(s"e1: ${e1}")
  }
}   

In our system compiler guards possible operations. For example energy can’t be summed with velocity , or velocity multiplied by velocity. We also get implicit conversions for free, if we wanted to, just by using: import scala.language.implicitConversions.