1

I took the following code from

Apple Inc.. (2022). "Opaque Types — The Swift Programming Language (Swift 5.7)"

Swift documentation example:

// main :: IO ()
func main() -> () {
       let shape = join(
               Triangle(size: 3),
               flip(Triangle(size: 3))
       )
       return print(
               shape.draw()
       )
}

protocol Shape {
       func draw() -> String
}

func flip<T: Shape>(_ shape: T) -> some Shape {
       return FlippedShape(shape: shape)
}

func join<T: Shape, U: Shape>(_ top: T, _ bottom: U) -> some Shape {
       JoinedShape(top: top, bottom: bottom)
}

struct FlippedShape<T: Shape>: Shape {
       var shape: T
       func draw() -> String {
               let lines = shape.draw().split(separator: "\n")
               return lines.reversed().joined(separator: "\n")
       }
}

struct JoinedShape<T: Shape, U: Shape>: Shape {
       var top: T
       var bottom: U
       func draw() -> String {
               return top.draw() + "\n" + bottom.draw()
       }
}

struct Triangle: Shape {
       var size: Int
       func draw() -> String {
               var result: [String] = []
               for length in 1...size {
                       result.append(String(repeating: "*", count: length))
               }
               return result.joined(separator: "\n")
       }
}

main()

I am wondering how can I translate it to Haskell. The end goal is joining a Triangle datatype with the flipped version of a Triangle datatype. This is my attempt:

Haskell example:

module Main where

import Data.List.Extra

class Shape a where
 draw :: a -> String

newtype Triangle = Triangle Int
newtype Square = Square Int

instance Shape Triangle where
 draw (Triangle n) = unlines $ take n $ iterate ('*' :) "*"

flip :: Shape a => a -> a
flip shape = flippedShape shape

join :: Shape a => a -> a -> a
join top bottom = joinedShape top bottom

flippedShape :: Shape a => a -> a
flippedShape = unlines . reverse . splitOn "\n" . draw

joinedShape :: Shape a => a -> a -> a
joinedShape top bottom = draw top <> "\n" <> draw bottom

main :: IO ()
main = print $ Main.join (Triangle 3) (Main.flip (Triangle 3))

So, I have two questions:

  • How can I translate the Swift example into Haskell ?, and
  • What's the Haskell equivalent for some (Opaque type) ?
F. Zer
  • 1,081
  • 7
  • 9
  • Existential types and type classes can often lead to a Haskell anti-pattern. Use them with care. instead of having (pseudo-syntax) `exists t. (t, t->String)` it's simpler to just use the isomorphic type `String`. Indeed, an opaque type that supports no operation except "convert to string" is just a string, up to isomorphism. (This is also justifed in theory by the "Coyoneda" lemma). Sometimes existential types are indeed useful, but I would not use them unless there are no simpler options. – chi Jan 13 '23 at 22:08
  • Thank you, @chi. Do you thing Opaque types and existential types relate ? Is that why the word `some` is used ? – F. Zer Jan 16 '23 at 01:49
  • 1
    Yes, existential types can be used to hide the internal representation and only offer a few abstract operations, which is close to what happens with opaque types. – chi Jan 16 '23 at 09:31

1 Answers1

1

The most direct translation adds a shape data type to supplement the shape type class.

class Shape a where draw :: a -> String

newtype SomeShape = SomeShape String
-- if desired, can make this instance:
instance Shape SomeShape where draw (SomeShape s) = s

flip :: Shape a => a -> SomeShape
flip s = SomeShape (unlines . map reverse . lines $ draw s)

This case is a bit too simple to really capture all the exciting bits of opaque types, but because of that simplicity I would also consider using String directly (or a difference-list equivalent if efficiency is a concern), with no type class or additional data type.

type Shape = String

triangle :: Int -> String
triangle n = {- ... -}

flip :: Shape -> Shape
flip = unlines . map reverse . lines

joinVertical :: Shape -> Shape -> Shape
joinVertical s1 s2 = s1 ++ "\n" ++ s2
Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380
  • This is the way. I would even go further, drop the class and have `Shape { draw :: String }`. There's a common misconception that OO interfaces have to be encoded as classes. It's very freeing to give it up. If the only way to use a thing is its interface, then the interface is the thing. Would also recommend [Codata in action](https://www.microsoft.com/en-us/research/uploads/prod/2020/01/CoDataInAction.pdf). – Li-yao Xia Jan 13 '23 at 22:34
  • 1
    @Li-yaoXia Check the updated answer -- I agree with you. =) – Daniel Wagner Jan 13 '23 at 22:34
  • I know you do :) – Li-yao Xia Jan 13 '23 at 22:37
  • Thank you both of you. @Daniel Wagner, this is spectacular. Could you tell me which are some of the exciting bits of Opaque types ? Do they exist in Haskell ? – F. Zer Jan 15 '23 at 18:13
  • @F.Zer Things get more difficult if the type being made opaque appears in both positive and negative positions in the class methods' types -- e.g. if you can update an existing value of the type -- or if there are methods that accept multiple arguments of the same opaque type. – Daniel Wagner Jan 16 '23 at 00:14
  • @Daniel Wagner, thank you. Where can I find out more about negative and positive positions, in this context ? – F. Zer Jan 17 '23 at 00:55
  • isn't the definition `flip s = SomeShape (unlines . reverse . lines $ draw s)`, without `map` ? Not sure why you are reversing each line, instead of reversing the list of lines. – F. Zer Jan 17 '23 at 00:55
  • 1
    @F.Zer You can read more about positive and negative positions in my edit on [this answer](https://stackoverflow.com/questions/9243215/lifting-a-higher-order-function-in-haskell/9243982#9243982). Nothing deep going on with `map` vs not; I just don't know swift's standard library, so I took a guess at what `reversed` did (and missed, apparently). – Daniel Wagner Jan 17 '23 at 01:02