Skip to content

Instantly share code, notes, and snippets.

@tdoris
Last active November 10, 2023 01:15
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tdoris/de36d2306edc5d6e9e7d to your computer and use it in GitHub Desktop.
Save tdoris/de36d2306edc5d6e9e7d to your computer and use it in GitHub Desktop.

Does Haskell make promises it can't keep?

or The big problem with wrapping numeric types

When coding financial calculations, it's common to have a variety of numeric values such as Quantity, Price, Amount and so on. In quant finance, these usually refer to the Price observed in the market, a Quantity of shares. A Quantity of shares all traded at a given Price in a given currency will give you an Amount of that currency (the total value of the trade).

It's simple and fun exercise to write the functions you'd need to perform all the correct ways of combining Price, Quantity and Amount. There are a couple of things that don't work as you might expect (e.g., multiplication is ok but you need to think about division semantics.).

Many Haskell tutorials encourage the coder to wrap Double and Int in newtypes like this, so the compiler can help by detecting incorrect combinations of the different types, and so the code documents itself better. e.g.

-- newbie:
tradeValue :: Int -> Double -> Double

-- better:
tradeValue :: Quantity -> Price -> Amount

The easy problem

The first problem you might hit after creating these wrapped types is that you can't perform the operations that you'd expect on values of the same type. For example

newtype Amount = MkAmount Double

foo :: Amount -> Amount -> Amount
foo a b = a + b

This code will give you an error because there's no '+' operation for your Amount type yet. Thankfully, this is easily fixed with a deriving clause and a language pragma.

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

newtype Amount = MkAmount Double deriving (Num)

Now the clever compiler knows you want it to make Amount an instance of the Num typeclass, and it will take care of using the same operations as the Num instance for Double.

Note that deriving an instance of Num for your numeric types doesn't mean that you can mix them in an expression as though they are all the same type, which is correct, otherwise we'd just be back where we started and Price, Quantity and Amount could be used in the same expressions with no controls.

The bigger problem

But let's say we have a list of Price, and we want to calculate the mean, or the 75% percentile. Of course we shouldn't need to roll our own implementation, Haskell has a couple of great packages that provide these functions. So we look up the function we want, and there it is:

mean :: Vector Double -> Double

Of course it makes sense that numerical packages have Vector Double in their function parameters; but it means we have to cast our Price into Double and only then pass it into the mean function, and we have to think about casting the result Double back into Price.

So far, so underwhelming. Everyone knows this, what's the problem? First, just so we're clear, it doesn't have to be like this, there is a better way. The numerical packages could write their interfaces using higher level abstractions. For instance, if mean had this signature, there would be no problem:

mean :: (Foldable f, Fractional a) => f a -> a

This function tells us we can pass any container that is Foldable containing any Fractional type. It takes a little more time to understand when reading the signature, but it's far more general. You could argue that this signature is more informative; when Double is used in a function signature, the function could be using any operations from any typeclasses that Double is a member of. If the value is constrained to be a Fractional, you can be more sure that the function is just going to use operations from the Fractional typeclass.

The impact

The example shown is a fairly trivial function with a simple type signature. It's possible that the generalised signatures for a proper library of numerical functions would become quite difficult to read and understand. It's also possible that signatures like this would unnecessarily constrain the implementations; the library authors might discover a better algorithm for implementing the same function, but if it requires operations not in the typeclasses they've used in the signature, they have to add another function with a similar name (confusing), or break backwards compatibility.

But for now the fact is that most if not all of Haskell's leading numerical packages just use Double in their signatures. So the impact of this problem is that if you follow best practices and create newtypes for your types, you'll have to cast them to and from Double to use the library functions.

This is one of those problems that doesn't get people too excited at first. But once a codebase contains these casts to use these library functions, they spread, and it's difficult to maintain the discipline of only using them where they're absolutely necessary. The point of strong semantic types is that discipline can be imposed by the compiler. That's the huge advantage and that's what we're losing here.

@dmcclean
Copy link

The advent of Data.Coerce.coerce helps this problem a bit, because in a context where MkAmount is exposed, you can write:

meanAmount :: Vector Amount -> Amount
meanAmount = coerce mean

It's also possible to write this inline if the context is sufficient to determine the type.

It's also possible to address this concern: "It's also possible that signatures like this would unnecessarily constrain the implementations; the library authors might discover a better algorithm for implementing the same function, but if it requires operations not in the typeclasses they've used in the signature, they have to add another function with a similar name (confusing), or break backwards compatibility." using GHC's {-# RULES #-} machinery, but it's fairly involved.

@andrejbauer
Copy link

I agree about mean, but is it really so difficult to write foo (MkAmount a) (MkAmount b) = MkAmount (a + b)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment