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?
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.
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, eg 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)
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...
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
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"
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.