简体   繁体   中英

Haskell: How to organize a group of functions that all take the same arguments

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM