0
votes

I'm trying to write a function that is polymorphic on a String or Num argument in Haskell.

I've written some pseudocode that doesn't compile:

numDigits strOrNum = 
  if isString strOrNum then length strOrNum
  else isNum strOrNum then length (show strOrNum)

numDigits 1000   -- Should return 4
numDigits "1000" -- Should return 4

Note isString and isNum are not real Haskell functions, they're just for demonstration.

Coming from Lisp, strOrNum is a union type, and this code would run.

I know typeclasses are needed for ad-hoc polymorphism in Haskell, but I'm not sure how to piece it together.

3
you could use type-classes for this but you'll run into issues like overlapping/undecidable instances soon for the way you want to do this - I'd ask myself the question: Do I need really need this? And if yes: is a data MyNum = IntNum Int | StringNum String good enough? (if you do a Num and IsString instance for that type you can get numDigits .. as you wanted working with the OverloadedStrings extensionRandom Dev

3 Answers

9
votes

Whether it's better to model something as a sum-type or a type-class is I think an intuition that you simply build over time (sum-types are more often the better option).

But since you mention

I know typeclasses are needed for ad-hoc polymorphism in Haskell, but I'm not sure how to piece it together.

here's a hopefully intuitive explanation.

Basic idea

Basically you're saying that there is a whole selection of types that your code can work with. You should try to formulate what those types have in common - i.e. why do you accept those types but reject others. You then define a type-class that captures this and implement most of your code in terms of this type-class.

Your example

To illustrate it on your example. You're accepting numbers and strings (that represent numbers) because you say that both can be represented as sequences of digits and you want to define a function that counts those digits. It probably then makes sense to define a type-class that captures the ability to be written as a sequence of digits.

class IsBase10Positional t where
  digits :: t -> [Int]

you'd then define the instances for your types:

instance IsBase10Positional Int where
    digits n = D.digits 10 n

instance IsBase10Positional String where
    digits chars = digitToInt <$> chars

D.digits comes from http://hackage.haskell.org/package/digits-0.3.1/docs/Data-Digits.html#v:digits

digitToInt from https://hackage.haskell.org/package/base-4.15.0.0/docs/Data-Char.html#v:digitToInt

your numDigits function is then defined on all types that are part of this type-class:

numDigits :: IsBase10Positional a => a -> Int
numDigits x = length (digits x)

Conclusion

It is often pretty hard to formulate what is the common behavior that the type-class is supposed to capture. If you start adding a lot of functions into the type-class itself then you probably failed to capture the right essence of what the types have in common (in your application's domain).

5
votes

You can this kind-of working with this:

data MyNum
  = IntNum Int
  | StrNum String

numDigits :: MyNum -> Int
numDigits (IntNum n) = length $ show n
numDigits (StrNum s) = length s

instance Num MyNum where
  fromInteger = IntNum . fromInteger
  -- note: skipped the rest (won't work really well sorry)

instance IsString MyNum where
  fromString = StrNum

here is an example in GHCi:

> :set -XOverloadedStrings
> numDigits "1000"
4
> numDigits 1000
4

This is working as 1000 as a literal is considered a type of any Num - fromInteger will be used to convert this into the target-type.

Same for "1000" when you enable the OverloadedStrings extension in GHC this will be treated as an IsString instance using fromString

I'd consider this just a nice trick for this question though - the Num instance is obviously incomplete.

I'd have to give this some thought, but I think this cannot be turned into a lawful Num instance because of the StrNum part (allowing for StrNum "bad" for example)

My hunch is that you get into trouble because you'd have to use StrNum ... values a both 0 and 1 and if those both are equal the resulting ring can only really be {0} (if you even allow for it)

Note: this is no proof and I can (and probably am) wrong ...

3
votes

Maybe this is what you want?

{-# LANGUAGE FlexibleInstances #-}
class HasNumDigits a where
  numDigits :: a -> Int

instance HasNumDigits Int where
  numDigits n = length (show n)

instance HasNumDigits String where
  numDigits s = length s

-- >>> numDigits (1000 :: Int)
-- 4

-- >>> numDigits "1000"
-- 4

Alternatively, a sum type is often a better alternative than a union type:


data StrOrNum = Num Int | Str String

numDigits strOrNum = case strOrNum of
  Str str -> length strOrNum
  Num num -> length (show strOrNum)

-- >>> numDigits (Num 1000)
-- 4

-- >>> numDigits (Str "1000")
-- 4