LINUX.ORG.RU

Чистые функции и внешние данные.

 ,


0

3

Имеется такая ситуация. Пишем прогу на хаскеле, по всем функциональным канонам стараясь максимально выносить код в чистые функции. В один прекрасный момент становится ясно, что некоей функции f очень нужны внешние данные (из файла, сети и т.д.). Как правильно поступить?

1. Заюзать unsafePerformIO - быстро, просто, но не кошерно.

2. Добавить еще один аргумент, те самые внешние данные, получить которые теперь задача вызывающего.

3. Завернуть функцию в IO и переделать вызов в монадический.

4. Есть еще варианты?


Я так понимаю, вариант «спроектировать с самого начала чтобы таких прекрасных моментов не случалось» не рассматривается?

hateyoufeel ★★★★★
()

мне кажется лучше третий

// мимо-не-хачкеллист

Debasher ★★★★★
()

Как правильно поступить?

Выбрать другой инструмент.

Oxdeadbeef ★★★
()

Так же, как и в любом другом языке, 2.

anonymous
()

Пункт 2, конечно. Если необходимо, совместить с пунктом 3, т.е. сделать обёртку в IO над f, которая будет получать данные, а f всё равно оставить чистой.

unC0Rr ★★★★★
()
Последнее исправление: unC0Rr (всего исправлений: 1)
Ответ на: комментарий от hateyoufeel

спроектировать с самого начала

Ну вот примерчик

translate :: String -> String
translate s = lookup s lookupData
   where
          lookupData = [("козел", "начальник"),
                        ("соска", "секретарша"),
                        ("самки дятла", "сотрудницы"),
                        ("стадо баранов", "коллектив")]

сначала данных немного, но потом lookupData может разрастись до 100500 строк. А может и не разрастись. Проектировать все под «авось понадобится расширять» как-то дофига мартышкиного труда. Или у вас есть тайное знание, как предвидеть то место где понадобится расширение?

chuzhoi
() автор топика
Ответ на: комментарий от chuzhoi

как-то дофига мартышкиного труда. Или у вас есть тайное знание, как предвидеть то место где понадобится расширение?

Тут что-нибудь одно, либо дофига матрышкиного труда, либо отказ от функционального, третьего не дано.

aboutcard
()
Ответ на: комментарий от unC0Rr

Пункт 2, конечно.

Тут главный недостаток - сильно связанный код. Вышестоящая функция должна знать, как грузить ресурсы для нижестоящих и как потом передать эти ресурсы дальше, через несколько слоев.

chuzhoi
() автор топика
Ответ на: комментарий от chuzhoi

было

i'am'pure :: Input -> Output
стало
i'm'pure'but'want'some'data :: Data -> Input -> Output
i'can'read'data  :: IO Data
i'm'function'in'user'interface input :: Intput -> IO Output
i'm'function'in'user'interface input = do
   d <- i'can'read'data
   return i'm'pure'bute'want'some'data

для юзера только Output на IO Output поменяется.

qnikst ★★★★★
()
Ответ на: комментарий от qnikst

А при чем тут want some data? тут нет никакого but, чистая функция и так получает данные безо всяких костылей, просто изменяего состояния нет.

aboutcard
()
Ответ на: комментарий от hateyoufeel

Я так понимаю, вариант «спроектировать с самого начала чтобы таких прекрасных моментов не случалось» не рассматривается?

намекаешь не использовать Haskell? Тонко.

umren ★★★★★
()

Не знаю как там в Haskell, но 100% чистых функций ты нигде не получишь, поэтому пункт 2, с некоторым количеством грязных функций.

umren ★★★★★
()

очень нужны внешние данные (из файла, сети и т.д.)

Они нужны при каждом вызове или единожды (в смысле, данные одни и те же)? Если единожды, то всё-таки unsafePerformIO. Например, чтение конфигурации часто так делается.

А если при каждом выполнении данные разные, то

- если данные всегда только из одного места, тогда IO

- если данные потенциально могут быть из разных мест, то параметр Data. Если, в основном, место одно, делаешь обёртку в IO, которая читает данные в Data и передаёт в функцию, экспортируешь и функцию и обёртку.

monk ★★★★★
()
Ответ на: комментарий от qnikst

для юзера только Output на IO Output поменяется.

т.е. вместо чистой функции, делать функцию в монаде IO ? Но ведь это потребует переделки всего вызывающего кода(мелочи в принципе), и к тому же IO не протащить через чистые функции (вот это серьезно) .

Не, с теорией i'can'read'(IO) / i'm'pure(не IO) это понятно. Интересует решение практической задачки с лукапами.

chuzhoi
() автор топика
Ответ на: комментарий от anonymous

Не проще ли взять нормальный ЯП?

Чтобы не было удивления, от того, что функция sin вдруг ещё и запись в БД делает. Неожиданных глюков меньше.

monk ★★★★★
()
Ответ на: комментарий от monk

Поправочка: Получишь, если ты пишешь либу без клиентского кода только.

umren ★★★★★
()
Ответ на: комментарий от monk

Весь math.h, например.

Видимо, это исключение. Математика сама по себе чистая и абсолютно бесполезная игрушка, поэтому она и проецируется на фп.

Разница между математическим подходом и оъектно-ориентированным: есть яблоко, надо его разделить не 2 части. Берешь функцию, скажем, x => x / 2, применяешь к объекту, в итоге у тебя есть целое яблоко, и выхлоп функции, которое показывает тебе половину яблока, которое гипотетически могло возникнуть в результате деления яблока. Однако, в реальности яблоко как было, так и осталось.

by contrast.

В ООП-подходе, ты реально делишь яблоко, у тебя целое яблоко исчезает, а 2 его части появляются.

Не надо иметь много мозгов даже, чтобы понять, что математический подход неадекватен, он представляет нам соверщенно не то, что мы ожидаем от него. Более того, он даже неспособен адекватно отображать процессы реального мира. И, собственно, даже процессы абстрактных миров, так как последние являются так или иначе проекциями реального мира. То бишь вообще ни о чем. По сути, математический подход отображает нам сам же математический подход, и ничего более. А далее идут фокусы, чтобы это заставить как-то работать.

Можно дергать зубы напрямую, через рот, а можно ненапрямую, через жопу. Результат от этого не изменится, это повлияет тоько на скорость, в данном случае — скорость разработки.

aboutcard
()
Ответ на: комментарий от monk

Чтобы не было удивления, от того, что функция sin вдруг ещё и запись в БД делает. Неожиданных глюков меньше.

подобные глюки возникают иключительно из-за кривых рук. Они являются неожиданными только для клоунов.

aboutcard
()
Ответ на: комментарий от aboutcard

Берешь функцию, скажем, x => x / 2, применяешь к объекту, в итоге у тебя есть целое яблоко, и выхлоп функции, которое показывает тебе половину яблока, которое гипотетически могло возникнуть в результате деления яблока. Однако, в реальности яблоко как было, так и осталось.

/me рыдает

hateyoufeel ★★★★★
()
Ответ на: комментарий от chuzhoi

у тебя этот файл нужно в рантайме читать и он может меняться? если да, то не выпендриться и иметь функцию в IO примерно как я написал и менять код вызывающий, т.к. он неожиданно перестал быть чистым. Если не меняется в рантайме, то через TH считай файл и сгенерь код, будет чистый. Возможны промежуточные варианты, где есть функция mkPure :: IO (Input -> Output), которая скачивает/читает, что нужно и возвращает чистую функцию, с которой работает вызывающий. В целом тяжело сказать, не представляя, что ты там делаешь.

qnikst ★★★★★
()

2 или 3, по вкусу или ситуации.

KblCb ★★★★★
()
Ответ на: комментарий от bookman900

не, похоже, что ТС все же другой человек, или очень старается.

qnikst ★★★★★
()

А ведь и правда интересный вопрос. Можно ли в хаскелле спроектировать программу с loose coupling компонентов, чтобы кому не надо, тому было все равно, есть там IO, нет IO или что как вообще? Я представляю, как это делается только в ООП языках, а вот в хаскелле - не знаю. Не может же быть, что все так плохо?

hlebushek ★★
()
Ответ на: комментарий от anonymous

Согласен!

На простых вещах ТС спотыкается. То ли еще будет, когда начнутся вещи сложные! :)

d_Artagnan ★★
()
Ответ на: комментарий от hlebushek

Можно ли в хаскелле спроектировать программу с loose coupling компонентов

Да.

чтобы кому не надо, тому было все равно, есть там IO, нет IO или что как вообще

Это аналогично требованию для C++: сделать функцию, чтобы её результат был в контейнере или без, вызывающему функцию должно быть всё равно.

monk ★★★★★
()
Ответ на: комментарий от qnikst

у тебя этот файл нужно в рантайме читать и он может меняться?

Вот же пример того, что у меня имеется. Сначала lookupData была 5 строк. Затем выросла до 100500. Держать это как константу в исходном коде уже неудобно. Хорошо бы вынести это в отдельный файлик.

Это пока все, но завтра, я хз.., может понадобиться выбрать его из нескольких или прочесть по сети. Вопрос в том, как правильно спроектировать код в этом месте, чтобы последующие возможные изменения были минимально затруднительными.

chuzhoi
() автор топика
Ответ на: комментарий от hlebushek

Можно ли в хаскелле спроектировать программу с loose coupling компонентов,

да, не понимаю, в чем проблема..

чтобы кому не надо, тому было все равно, есть там IO, нет IO или что как вообще?

это не loose coupling, за исключением варианта, что кто-то использует слишком много слоёв в монадном стеке, просто потому, что нравится.

есть там IO, нет IO или что как вообще?

userfunction :: Monad m => Input -> m Output

этой функции все равно есть там IO или нету, единственне, что если фунция чистая то запускать ей надо будет через runIdentity.

иногда можно сделать что-то типа

data Goo * :: where
  Action :: Input -> Goo (IO Output)
  Pure   :: Input -> Goo Output

gohem :: Goo a -> a
gohem = ...

Теперь компилятор выведет есть там эффекты или нет в зависимости от входа.

Я представляю, как это делается только в ООП языках, а вот в хаскелле - не знаю.

ну я, например, не представляю как в ООП языках вообще сделать контроль за эффектами.

qnikst ★★★★★
()
Ответ на: комментарий от qnikst

А что там представлять? Для использования муттабельных данных порождай всегда новый объект, и все. Это те же самые монады, только лучше.

anonymous
()
Ответ на: комментарий от chuzhoi

Вот же пример того, что у меня имеется. Сначала lookupData была 5 строк. Затем выросла до 100500. Держать это как константу в исходном коде уже неудобно. Хорошо бы вынести это в отдельный файлик.

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

{-# LANGUAGE TemplateHaskell #-}

module T where
import Language.Haskell.TH
import qualified Data.Map as Map
import Data.Maybe

createTranslateTable :: FilePath -> ExpQ
createTranslateTable fn = do
    d  <- fmap catMaybes . mapM toTupleQ
              . map words . lines =<< runIO (readFile fn)
    [| Map.fromList d |]
  where
    toTupleQ [a,b] = return $ Just (a,b)
    toTupleQ x = do
      reportWarning $ "Wrong structure: " ++ unwords x
      return Nothing

+
{-# LANGUAGE TemplateHaskell #-}
module Main where

import T

main = print $(createTranslateTable "1.txt")

вариант №2:


type Translation = String -> String

mkTranslationData :: Map String String -> Translation
mkTranslationData = flip Map.lookup

mkTranslationFile :: FileName -> IO Translation
mkTranslationFile fn = mkTranslationData . read <$> readFile fn

mkTranslationNetwork :: String -> IO Translation
mkTranslationNetwork fn = ..

main = do
  translation  <- mkTranslationFromFile "foo"
  translation2 <- mkTranslationFromNetwork "http://somewhere.net/"
  let result = purecode (... translation "Foo" ... translation2 "Bar")

хороший вопрос тут, тот, что задал hlebushek, а что если я хочу сделать вариант Translation, который делает в сеть запрос на каждое слово, но выше отвечено и на это, хотя и не подробно. (Ну и ты это не спрашивал).

qnikst ★★★★★
()

1 — суксь, не делай так. 2,3 — это «завернуть в монаду», только монады разные (Reader и IO). Собственно, отсюда вывод — заверни в монаду собственного тайп-класса, потом разберёшься, какую именно.

Miguel ★★★★★
()

Блин, вот это да. Собралась толпа неосиляторов нормального программирования, анально отгороженных, и обсуждают говнопроблемы недоязыка, как решить детскую проблему. Когда же вы код будете писать, если у вас все силы на борьбу с собственной жопой уходят? Теперь я понимаю, откуда корни жабаничтожества растут.

anonymous
()
Ответ на: комментарий от qnikst
translation  <- mkTranslationFromFile "foo"
  translation2 <- mkTranslationFromNetwork "http://somewhere.net/"
  let result = purecode (... translation "Foo" ... translation2 "Bar")

Ух ты, в хаскеле оказывается такие же присваивания имеют место быть, как и в моем любимом вижуалбейсике.

anonymous
()
Ответ на: комментарий от chuzhoi

Как-то так, наверно

translateWithStaticLookup :: IO (String -> Maybe String)
translateWithStaticLookup = return (translatePure [("козел", "начальник"),
                                     ("соска", "секретарша"),
                                     ("самки дятла", "сотрудницы"),
                                     ("стадо баранов", "коллектив")])

translateWithFileLookup :: FilePath -> IO (String -> Maybe String)
translateWithFileLookup = ...

translatePure :: [(String, String)] -> String -> Maybe String
translatePure lookupData str = lookup str lookupData

main = do
    translate <- translateWithStaticLookup
    putStrLn (show (translate "козел"))

anonymous
()
Ответ на: комментарий от monk

Это аналогично требованию для C++: сделать функцию, чтобы её результат был в контейнере или без

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

Твоя аналогия по условию меняет интерфейс, что в общем-то не несет проблем.

A1
()
Ответ на: комментарий от A1

Твоя аналогия по условию меняет интерфейс

Твоя тоже. Функция возвращает значение, а ты от неё хочешь возврата действия, меняющего мир.

monk ★★★★★
()
Ответ на: комментарий от anonymous

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

qnikst ★★★★★
()
Ответ на: комментарий от qnikst

Я не знаю ни одного ООП языка

А ты вообще то их знаешь? Умеешь?

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.