/purescript-sytc

Scala-like traits in PureScript

Primary LanguagePureScript

purescript-sytc

Scrap your (PureScript) typeclasses.

Background

Type classes are ways to define the behavior of a function for certain types. Here's an example:

class Show a where
  show :: a -> String

instance showString :: Show String where
  show = identity

instance showInt :: Show Int where
  show i
    | i == 0 = "0"
    | i > 0 = "1 + " <> show (i - 1)
    | i < 0 = "-1 + " <> show (i + 1)

Type classes live in a global scope. The compiler stores all the instances as "rows" of a table. So Show above would be a table and showString would be a row, as would showInt. Then, when it finds a value of type x, it looks in the table to see if there is an instance (row) of the type class that can handle x. If so, it plugs in that instance. If not, it raises a compiler error.

prog = do
  log $ show 1 -- ok
  log $ show "hello" -- ok
  log $ show unit -- compiler error because we haven't defined Show Unit

Scrap your type classes

Type classes make a lot of sense when you're working on a library and you don't know who will be using it. In that case, they provide a global namespace so that, if you make an instance of Show and that instance gets imported (directly or transitively) into a project, the compiler will use it.

On the other hand, when building applications, we often want composable segments that represent business logic. For example, if we have a complicated record with many fields, we'll usually split it up into smaller records. For example:

type FirstName r = (firstName :: String + r)
type LastName r = (lastName :: String + r)
type Person r = (FirstName + LastName + r)
--- and later, on the application layer, we close the record
type SimplePerson = Record (Person + ())

This library provides a set of tools for using typeclasses as one would use extensible records. In the example below, intShow is composed with boolShow much like, in the example above, FirstName is composed with LastName.

module Main where

import Prelude
import Control.Lazy (fix)
import Data.Newtype (class Newtype)
import Data.Typeclass (using, Typeclass, type (@@), (<@@>), type (@>), TNil, (@>), tnil)
import Effect (Effect)
import Effect.Class.Console (log)

newtype ShowMe a
  = ShowMe (a -> String)

derive instance newtypeShowMe :: Newtype (ShowMe a) _

type Showable :: forall k. k -> Type
type Showable t
  = Typeclass (ShowMe @@ (t @> TNil))

showable :: forall t. (t -> String) -> Showable t
showable x = (ShowMe x) @> tnil

intShow :: Showable Int
intShow = showable $ fix \f i ->
  if i > 0 then "1 + " <> f (i - 1)
  else if i < 0 then "-1 + " <> f (i + 1)
  else "0"

intShowAlt :: Showable Int
intShowAlt = showable $ fix \f i ->
  if i > 0 then "one plus " <> f (i - 1)
  else if i < 0 then "negative one plus " <> f (i + 1)
  else "zero"

boolShow :: Showable Boolean
boolShow = showable $ if _ then "true" else "false"

main :: Effect Unit
main = do
  log $ using (intShow <@@> boolShow) true
  log $ using (intShow <@@> boolShow) 5
  log $ using (intShowAlt <@@> boolShow) 5
  log $ using intShow (-1)

This produces:

true
1 + 1 + 1 + 1 + 1 + 0
one plus one plus one plus one plus one plus zero
-1 + 0

It is similar in some ways to the scrap your typeclass article from 2012 with the major caveat that it still allows for parametric polymorphism. I think this is a very useful feature that I'm not willing to scrap!

API

Here's an example showing how type classes can be treated as data that can be composed together.

module Basic where

import Prelude
import Data.Newtype (class Newtype)
import Data.Tuple.Nested ((/\))
import Data.Typeclass (type (@>), type (@@), TNil, Typeclass, tnil, using, (<@@>), (@-), (@>))
import Effect (Effect)
import Effect.Class.Console (log)
import Type.Proxy (Proxy(..))

newtype ShowMe a
  = ShowMe (a -> String)

derive instance newtypeShowMe :: Newtype (ShowMe a) _

type Show'
  = ShowMe @@ Int @> Boolean @> TNil

myShow :: Typeclass Show'
myShow =
  ShowMe (show :: Int -> String)
    @> ShowMe (\(_ :: Boolean) -> "Fooled you with a fake boolean!")
    @> tnil

yourShow :: Typeclass Show'
yourShow =
  (ShowMe $ \(_ :: Int) -> "Fooled you with a fake integer!")
    @> (ShowMe $ (show :: Boolean -> String))
    @> tnil

meanShow :: Typeclass Show'
meanShow =
  let
    _ /\ _ /\ t = (Proxy :: Proxy Int) @- myShow

    _ /\ h /\ _ = (Proxy :: Proxy Boolean) @- yourShow
  in
    h <@@> t

niceShow :: Typeclass Show'
niceShow =
  let
    _ /\ _ /\ t = (Proxy :: Proxy Int) @- yourShow

    _ /\ h /\ _ = (Proxy :: Proxy Boolean) @- myShow
  in
    h <@@> t

basic :: Effect Unit
basic = do
  log $ using myShow true
  log $ using myShow 1
  log $ using yourShow true
  log $ using yourShow 1
  log $ using niceShow true
  log $ using niceShow 1
  log $ using meanShow true
  log $ using meanShow 1

Produces:

**** basic
Fooled you with a fake boolean!
1
true
Fooled you with a fake integer!
true
1
Fooled you with a fake boolean!
Fooled you with a fake integer!

Induction

Recursively defined typeclasses can be created with the induction operator @!>.

At the typelevel, this takes a type initial /@\ iterator, where /@\ is an infix operator taking the initial value and the iterator. At the term level, it takes a value of type f (Proxy initial) /\ (forall x. (Proxy x) -> (f (Proxy x)) -> (f (Proxy (iterator x)))). Check out the example below:

module Recursive1 where

import Prelude
import Data.Newtype (class Newtype)
import Data.Tuple.Nested ((/\))
import Data.Typeclass
  ( type (/@\)
  , type (@!>)
  , type (@>)
  , type (@@)
  , TNil
  , Typeclass
  , tnil
  , using
  , (@!>)
  , (@>)
  )
import Effect (Effect)
import Effect.Class.Console (log)
import Type.Proxy (Proxy(..))

data Peano

foreign import data Z :: Peano

foreign import data Succ :: Peano -> Peano

newtype ShowMe a
  = ShowMe (a -> String)

derive instance newtypeShowMe :: Newtype (ShowMe a) _

type Show'
  = ShowMe @@ (Z /@\ Succ) @!> Boolean @> TNil

myShow :: Typeclass Show'
myShow =
  ShowMe (const "Z")
    /\ (\px (ShowMe f) -> ShowMe (const $ "Succ (" <> (f px) <> ")"))
    @!> (ShowMe (\(_ :: Boolean) -> "Fooled you with a fake boolean!"))
    @> tnil

recursive1 :: Effect Unit
recursive1 = do
  log $ using myShow true
  log $ using myShow (Proxy :: Proxy (Succ (Succ (Succ (Succ (Succ Z))))))
  log $ using myShow (Proxy :: Proxy Z)

This produces:

Fooled you with a fake boolean!
Succ (Succ (Succ (Succ (Succ (Z)))))
Z

More examples

More examples can be found in the tests.