0
votes

I'm new to Functional Programming. I've used Ramda a bit (JavaScript library), but nothing like the type system in Purescript.

I have an idea that I feel should be expressible with Purescript's type system, but I'm not really sure where to start.

Lets say I'm trying to define some types for a Sudoku Board

newtype Index = Index Int
newtype Column = Column Int
newtype Row = Row Int
newtype Box = Box Int

I'd like to define what addition looks like for these types

In sudocode:

indexAddition :: (Index | Int) -> (Index | Int) -> Index
indexAddition a b = (a + b) % 81

RowAddition :: (Row | Int) -> (Row | Int) -> Row
RowAddition a b = (a + b) % 9

ColumnAddition and BoxAddition can probably me merged with RowAddition since they're gonna be basically the same.

-- I have to be able to say that a is a subset of Int, but Int isn't a type class
FooAddition ::  forall a. Int a => a -> a -> a
FooAddition a b = (a + b) % 9

I somehow feel like I'm likely starting off on the wrong foot here.

Any help?

1

1 Answers

3
votes

To answer your question directly, the way to have a function that works with different types, but a limited set of them (also known as "overloaded function") is type classes. More specifically, such function should be a method of a type class, and then you create an instance for each type (or combination of types) you'd like it to work with.

So the most straightforward approach would be this:

class IndexAddition a b where
  indexAddition :: a -> b -> Index
instance addIntInt :: IndexAddition Int Int where
  indexAddition a b = Index ((a+b) % 81)
instance addIntIndex :: IndexAddition Int Index where
  indexAddition a (Index b) = Index ((a+b) % 81)
instance addIndexInt :: IndexAddition Index Int where
  indexAddition (Index a) b = Index ((a+b) % 81)
instance addIndexIndex :: IndexAddition Index Index where
  indexAddition (Index a) (Index b) = Index ((a+b) % 81)

As you can see, I made four instances, one for every combination of Index and Int. This works, but is admittedly a bit elaborate. Especially if you add a third parameter or a third possible type.

To make this a bit shorter and more manageable, you might observe that in order to add particular types, all you need from them is a way to convert to an Int. If you have that, you can convert both parameters to Int, then add, then wrap in Index:

class IsInt a where toInt :: a -> Int
instance ciIndex :: IsInt Index where toInt (Index a) = a
instance ciInt :: IsInt Int where toInt a = a

indexAddition :: forall a b. IsInt a => IsInt b => a -> b -> Index
indexAddition a b = Index ((toInt a + toInt b) % 81)

That said, I highly recommend that you reconsider your designs. Sure, ability to add numbers and indexes in any combination may look neat and nifty at first glance, but you probably will never need it in practice. And even if you do in some very specific circumstances, it's easy enough to just wrap/unwrap the values as needed. Trust me, I've been there. Many times.