11

I am writing a program with several functions that take the same arguments. Here is a somewhat contrived example for simplicity:

buildPhotoFileName time word stamp = show word ++ "-" ++ show time ++ show stamp
buildAudioFileName time word = show word ++ "-" ++ show time ++ ".mp3"
buildDirectoryName time word = show word ++ "_" ++ show time

Say I am looping over a resource from IO to get the time and word parameters at runtime. In this loop, I need to join the results of the above functions for further processing so I do this:

let photo = buildPhotoFileName time word stamp
    audio = buildAudioFileName time word
    dir   = buildDirectoryName time word
in ....

This seems like a violation of "Don't Repeat Yourself" principle. If down the road I find I would like to change word to a function taking word, I might make a new binding at the beginning of let expression like so:

let wrd   = processWord word
    photo = buildPhotoFileName time wrd stamp
    audio = buildAudioFileName time wrd
    dir   = buildDirectoryName time wrd
in ....

and would have to change each time I wrote word to wrd, leading to bugs if I remember to change some function calls, but not the others.

In OOP, I would solve this by putting the above functions in a class whose constructor would take time and word as arguments. The instantiated object would essentially be the three functions curried to time and word. If I wanted to then make sure that the functions receive processWord word instead of word as an "argument", I could call processWord in the constructor.

What is a better way to do this that would be more suited to Functional Programming and Haskell?

DJG
  • 6,413
  • 4
  • 30
  • 51

5 Answers5

11

Since you say you're ready to create an OO-wrapper-class just for that, I assume you're open to changing your functions. Following is a function producting a tuple of all three results you wanted:

buildFileNames time word stamp = 
  ( show word ++ "-" ++ show time ++ show stamp,
    show word ++ "-" ++ show time ++ ".mp3",
    show word ++ "_" ++ show time )

You'll be able to use it like so:

let wrd   = processWord word
    (photo, audio, dir) = buildFileNames time wrd stamp
    in ....

And if you don't need any of the results, you can just skip them like so:

let wrd   = processWord word
    (_, audio, _) = buildFileNames time wrd stamp
    in ....

It's worth noting that you don't have to worry about Haskell wasting resources on computing values you don't use, since it's lazy.

Nikita Volkov
  • 42,792
  • 11
  • 94
  • 169
  • 1
    Very nice. I've never seen this trick before though it's quite obvious in hindsight. A minor note: the three result strings share parts so I think you could insert some `let` -s to share those explicitly. – András Kovács Jun 18 '13 at 20:07
  • @AndrásKovács No need due to compiler performing [common subexpression elimination](http://stackoverflow.com/q/15084162/485115). – Nikita Volkov Jun 19 '13 at 12:49
7

The solution you described from OOP land sounds like a good one in FP land to me. To wit:

data UID = UID
    { _time :: Integer
    , _word :: String
    }

Including or not including the "stamp" in this record is a design decision that we probably don't have enough information to answer here. One can put this data type in its own module, and define a "smart constructor" and "smart accessors":

uid = UID
time = _time
word = _word

Then hide the real constructor and accessors at the module boundary, e.g. export the UID type, uid smart constructor, and time and word smart accessors, but not the UID constructor or _time and _word accessors.

module UID (UID, uid, time, word) where

If we later discover that the smart constructor should do some processing, we can change the definition of uid:

uid t w = UID t (processWord w)
Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380
6

Building on top of Nikita Vokov’s answer, you can use record wild cards for some neat syntax with little repetition:

{-# LANGUAGE RecordWildCards #-}

data FileNames = FileNames { photo :: String, audio :: String, dir :: String }

buildFileNames :: Word -> Time -> Stamp -> FileNames
buildFileNames time word stamp = FileNames
  (show word ++ "-" ++ show time ++ show stamp)
  (show word ++ "-" ++ show time ++ ".mp3")
  (show word ++ "_" ++ show time )

let FileNames {...} = buildFileNames time wrd stamp
in ... photo ... audio ... dir...
Joachim Breitner
  • 25,395
  • 6
  • 78
  • 139
  • @user1436026 In this example think of `FileNames` as your OOP object and `buildFileNames` as your constructor. The three records `photo`, `audio`, and `dir` are public attributes. They are each calculated once the first time you access them. – Michael Steele Jun 18 '13 at 21:01
4

Just to give you another example, if you are passing around the same parameters to multiple functions, you can use the Reader monad instead:

import Control.Monad.Reader

runR = flip runReader

type Params = (String, String, String)

buildPhotoFileName :: Reader Params String
buildPhotoFileName = do
  (time, word, stamp) <- ask
  return $ show word ++ "-" ++ show time ++ show stamp

main = do
  runR (time, word, stamp) $ do
    photo <- buildPhotoFileName
    audio <- buildAudioFileName
    dir <- buildDirectoryName
    processStuff photo audio dir
Vlad the Impala
  • 15,572
  • 16
  • 81
  • 124
0

To build on David Wagner's solution, and your OO objectives you should move the buildxxx function or functions to a separate module (NameBuilders?) That would give you complete control.

Even with this approach you should also "wrap" the variables with functions inside the module as David suggested.

You would export the variables and the buildxxx constructor (returning a triplet) or constructors (three separate functions).

you could also simplify by

    buildDirectoryName time word = show word ++ "_" ++ show time
    buildPhotoFileName stamp = buildDirectoryName + show stamp
    buildAudioFileName =       buildDirectoryName ++ ".mp3"