Haskell: Как организовать группу функций, которые принимают одинаковые аргументы

Я пишу программу с несколькими функциями, которые принимают те же аргументы. Вот несколько надуманный пример для простоты:

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

Скажем, я перебираю ресурс из IO, чтобы получить параметры time и word во время выполнения. В этом цикле мне нужно присоединиться к результатам вышеуказанных функций для дальнейшей обработки, поэтому я делаю это:

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

Это похоже на нарушение принципа "Не повторяй себя". Если по дороге я нахожу, что я хотел бы изменить word на функцию, принимающую word, я мог бы сделать новое связывание в начале выражения let следующим образом:

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

и мне придется менять каждый раз, когда я пишу word в wrd, приводя к ошибкам, если не помню, чтобы сменить некоторые вызовы функций, но не другие.

В ООП я решил бы это, поставив вышеуказанные функции в класс, конструктор которого примет в качестве аргументов time и word. Эквивалентным объектом будет, по существу, три функции, рассчитанные на time и word. Если бы я хотел убедиться, что функции получают processWord word вместо word как "аргумент", я мог бы вызвать processWord в конструкторе.

Каков наилучший способ сделать это, который больше подходит для функционального программирования и Haskell?

Ответ 1

Поскольку вы говорите, что вы готовы создать класс OO-wrapper только для этого, я предполагаю, что вы открыты для изменения своих функций. Ниже приведена функция, производящая кортеж из всех трех результатов, которые вы хотели:

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

Вы сможете использовать его так:

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

И если вам не нужны какие-либо результаты, вы можете просто пропустить их так:

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

Стоит отметить, что вам не нужно беспокоиться о том, что Haskell тратит ресурсы на вычисления значений, которые вы не используете, поскольку они ленивы.

Ответ 2

Решение, которое вы описали на земле ООП, звучит как хорошая в земле FP для меня. К остроумию:

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

Включение или отсутствие "штампа" в этой записи является конструктивным решением, которое, вероятно, не содержит достаточной информации для ответа здесь. Этот тип данных можно поместить в свой собственный модуль и определить "умный конструктор" и "интеллектуальные аксессоры":

uid = UID
time = _time
word = _word

Затем скройте реальный конструктор и аксессоры на границе модуля, например. экспортируйте UID тип, UID интеллектуальный конструктор и time и word интеллектуальные аксессоры, но не конструкторы UID или _time и _word.

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

Если позже мы обнаружим, что интеллектуальный конструктор должен выполнить некоторую обработку, мы можем изменить определение UID:

uid t w = UID t (processWord w)

Ответ 3

На вершине Никиты Вокова ответьте, вы можете использовать запись диких карт для некоторого аккуратного синтаксиса с небольшим повторением:

{-# 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...

Ответ 4

Чтобы дать вам другой пример, если вы передаете одни и те же параметры нескольким функциям, вместо этого вы можете использовать монаду читателя:

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

Ответ 5

Чтобы опираться на решение Дэвида Вагнера и ваши цели OO, вы должны переместить функцию или функции buildxxx в отдельный модуль (NameBuilders?), который даст вам полный контроль.

Даже при таком подходе вы также должны "обернуть" переменные функциями внутри модуля, как предложил Дэвид.

Вы экспортируете переменные и конструктор buildxxx (возвращаете триплет) или конструкторы (три отдельные функции).

вы также можете упростить

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