Translate Haskell Class-based ad-hoc polymorphism example to Scala

Hi all, I am having a go at translating the following Haskell into Scala:

data Circle = Circle Float
data Rect = Rect Float Float

class Shape a where
  area :: a -> Float

instance Shape Circle where
  area (Circle r) = pi * r^2
instance Shape Rect where
  area (Rect length' width') = length' * width'

main = do
  putStrLn (show (area(Circle 1)))
  putStrLn (show (area(Rect 2 3)))

Here is the current result:

  case class Circle(radius: Float)
  case class Rect(width: Float, height: Float)
  
  sealed trait Shape[A]:
    def area(shape: A): Double
  
  object Shape: 
  
    def area[S:Shape](shape: S): Double =
      summon[Shape[S]].area(shape)
  
    given circleShape: Shape[Circle] with
      def area(circle: Circle): Double =
        math.Pi * circle.radius * circle.radius

    given rectShape: Shape[Rect] with
      def area(rect: Rect): Double =
        rect.width * rect.height  
  
  import Shape.area
  
  assert(area(Circle(1)) == math.Pi)
  assert(area(Rect(2,3)) == 6)  

Are there any improvements that you can think of that would make it more faithful to the original?

Thanks,

Philip

Not necessarily more faithful to the original, but arguably more idiomatic and more concise:

abstract class Shape[A]:
  extension (shape: A) def area: Double

case class Circle(radius: Float)
object Circle:
  given Shape[Circle] =
    self => math.Pi * self.radius * self.radius

case class Rect(width: Float, height: Float)
object Rect:
  given Shape[Rect] =
    self => self.width * self.height  

assert(Circle(1).area == math.Pi)
assert(Rect(2,3).area == 6)  
3 Likes

Why abstract class instead of traits?

2 Likes

@LPTK Thank you very much for that.

It is going to take me a while to digest, as I am seeing a couple of things I have not come across before!

Philip

It’s needed for “single abstract method” class instantiation via a lambda. Besides, when choosing between an abstract class and a trait, I always pick the abstract class. It’s more straightforward and thus faster to compile since it maps directly to a Java abstract class, whereas traits do not (though this usually only makes a difference in big mixin composition use cases).

1 Like

I don’t think so, your example works with a trait too.

1 Like

Hah, interesting! I tried with a trait at first and Dotty returned an unhelpful message of the “could not infer parameter type” kind. On a tangential note, I found that Dotty tends to give up with this sort of errors much more easily than Scala 2, and when it does it’s often caused by a completely unrelated issue, which makes it quite difficult to work with. I’ll try to open an issue next time this happens to me.

2 Likes

Good news - this works and is pretty close to the original:

case class Circle(radius: Float)
case class Rect(length: Float, width: Float)

trait Shape[A]:
  extension (shape: A)
    def area: Double

given Shape[Circle] with
  extension (c: Circle)
    def area: Double = math.Pi * c.radius * c.radius

given Shape[Rect] with
  extension (r: Rect)
    def area: Double = r.length * r.width

@main def main: Unit =
  assert(Circle(1).area == math.Pi)
  assert(Rect(2,3).area == 6)
1 Like

The problem with your approach is that it works only because you have everything in scope. When you move things to different packages, the implicits won’t be found unless you explicitly import them. At least the compiler sometimes suggests this, but it’s still not ideal:

value area is not a member of lib.Circle, but could be made available as an extension method.

The following import might fix the problem:

  import lib.given_Shape_Circle

That’s the reason I put the instances in companion objects in my solution.

Hi @LPTK, right, let me see, I switched from bundling the two givens in an object to just having them on their own because I thought the importing ergonomics were reasonable. I was exploring the expression problem [1], and so I didn’t want to be forced to reopen an object in order to add a new given to it, I wanted additivity, the ability to add a new given in a new file, i.e. without having to touch existing files.

I just had a go at putting each program element in its own package, just to see what that forces me to do in terms of imports, and here is what I am seeing:

package explore.traits

trait Shape[A]:
  extension (shape: A)
    def area: Double

======================================================

package explore.data.circle

case class Circle(radius: Float)

======================================================

package explore.data.rect

case class Rect(length: Float, width: Float)

======================================================

package explore.givens.circle

import explore.traits.Shape
import explore.data.circle.Circle

given Shape[Circle] with
  extension (c: Circle)
    def area: Double = math.Pi * c.radius * c.radius

======================================================

package explore.givens.rect

import explore.traits.Shape
import explore.data.rect.Rect

given Shape[Rect] with
  extension (r: Rect)
    def area: Double = r.length * r.width

======================================================

package explore.main

import explore.data.circle.Circle
import explore.data.rect.Rect
import explore.givens.rect.given
import explore.givens.circle.given

@main def main: Unit =
  assert(Rect(2,3).area == 6)
  assert(Circle(1).area == math.Pi)

======================================================

Does the above showcase the ‘import inconvenience’ that you are keen to avoid, or possibly dispel your concerns?

[1]
https://homepages.inf.ed.ac.uk/wadler/papers/expression/expression.txt

http://channel9.msdn.com/shows/Going+Deep/C9-Lectures-Dr-Ralf-Lmmel-Advanced-Functional-Programming-Type-Classes/
http://ecn.channel9.msdn.com/o9/ch9/3672/563672/C9LecturesRalfLaemmelExpressionProblem_ch9.mp4
http://ecn.channel9.msdn.com/o9/ch9/7020/567020/C9LecturesRalfLaemmelAFPTypeClasses_ch9.mp4