Search code examples
classhaskelldefaultsubclassingrepeat

Set of functions that are instances in a common way


I'm pretty new to haskell and I think I'm falling into some OO traps. Here's a sketch of a structure (simplified) that I'm having trouble implementing:

  • A concept of an Observable that acts on a list of samples (Int) to produce a result (Int)

    • A concept SimpleObservable that achieves the result using a certain pattern (while there will be Observables that do it other ways), e.g. something like an average
  • A function instance, e.g. one that's just an average times a constant

My first thought was to use a subclass; something like (the below is kinda contrived but hopefully gets the idea across)

class Observable a where
  estimate :: a -> [Int] -> Int

class (Observable a) => SimpleObservable a where
  compute :: a -> Int -> Int
  simpleEstimate :: a -> [Int] -> Int
  simpleEstimate obs list = sum $ map compute list

data AveConst = AveConst Int

instance Observable AveConst where
  estimate = simpleEstimate

instance SimpleObservable AveConst  where
  compute (AveConst c) x = c * x

However, even if something like the above compiles it's ugly. Googling tells me DefaultSignatures might help in that I don't have to do estimate = simpleEstimate for each instance but from discussions around it it seems doing it this way would be an antipattern.

Another option would be to have no subclass, but something like (with the same Observable class):

data AveConst = AveConst Int

instance Observable AveConst where
  estimate (AveConst c) list = sum $ map (*c) list

But this way I'm not sure how to reuse the pattern; each Observable has to contain the complete estimate definition and there will be code repetition.

A third way is a type with a function field:

data SimpleObservable = SimpleObservable {
  compute :: [Int] -> Int
} 

instance Observable SimpleObservable where
  estimate obs list =
    sum $ map (compute obs) list

aveConst :: Int -> SimpleObservable
aveConst c = SimpleObservable {
  compute = (*c)
}

But I'm not sure this is idiomatic either. Any advice?


Solution

  • I propose going even simpler:

    type Observable = [Int] -> Int
    

    Then, an averaging observable is:

    average :: Observable
    average ns = sum ns `div` length ns
    

    If your Observable needs some data inside -- say, a constant to multiply by -- no problem; that's what closures are for. For example:

    sumTimesConst :: Int -> Observable
    sumTimesConst c = sum . map (c*)
    

    You can abstract over the construction of Observables without trouble; e.g. if you want a SimpleObservable which only looks at elements, and then sums, you can:

    type SimpleObservable = Int -> Int
    
    timesConst :: Int -> SimpleObservable
    timesConst = (*)
    
    liftSimple :: SimpleObservable -> Observable
    liftSimple f = sum . map f
    

    Then liftSimple . timesConst is another perfectly fine way to spell sumTimesConst.

    ...but honestly, I'd feel dirty doing any of the above things. sum . map (c*) is a perfectly readable expression without introducing a questionable new name for its type.