LINUX.ORG.RU

[Haskell] простой вопрос

 


3

4

Есть функция на Scheme (из sicp):

(define new-withdraw
  (let ((balance 100))
    (lambda (amount)
      (if (>= balance amount)
	  (begin (set! balance (- balance amount))
		 balance)
	  "Недостаточно денег на счете"))))
Как реализовать подобное на Haskell?

Можно представить как значение newWithdraw, возвращающее некоторое вычисление :: IO (Int -> Int)

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

Не совсем, скорее IO (Int -> IO Int).

newWithdraw :: IO (Int -> IO Int)
newWithdraw = do
  var <- newMVar 100
  return $ \amount -> do
    balance <- readMVar var
    if balance >= amount
      then do
        swapMVar var (balance - amount)
        readMVar var
      else error "Недостаточно денег на счете"
anonymous
()

1) В лоб, под лозунгом «императив — значит IO»: см. то, что написал анонимус.

2) Творчески. Два вида эффектов — изменение внутреннего состояния, и выдача ошибки. Отсюда — два ограничения:

newWithdraw :: (MonadState Integer m, MonadError String m) => m (Integer -> m Integer)
newWithdraw =
  do put 100
     return $
       \amount ->
         do balance <- get
            if amount <= balance
              then do let newBalance = balance - amount
                      put newBalance
                      return newBalance
              else throwError "Недостаточно денег на счёте"

3) Правильно. Нефиг мешать в кучу возврат ошибок и реальную работу.

newtype Balance = Balance (Integer -> Maybe (Integer, Balance))
newWithdraw :: Balance
newWithdraw = newWithdraw' 100 where
  newWithdraw' balance =
    Balance $ \amount ->
      do let newBalance = balance - amount
         guard $ newBalance >= 0
         return (newBalance, newWithdraw' newBalance)
Miguel ★★★★★
()
Ответ на: комментарий от anonymous

> Никак, в хаскеле нет аналога оператора set!

import Data.IORef

tensai_cirno ★★★★★
()

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

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

Да-да, особенно много монадических костылей во втором варианте Мигеля.

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

Ещё раз: зачем? Я не знаю ни одного случая, когда прямо из рабочей функции нужно было бы возвращать строку, описывающую ошибку. Объект, описывающий ошибку - да, это я понимаю, но финальную строчку?

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

Функционал повторён. Три раза, кстати.

А сообщения об ошибках — дело другое.

Да, Хаскель провоцирует разделять несвязанные друг с другом вещи.

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

Прямой аналог - никак нельзя.

Прямым аналогом была бы функция которая имеет разные значения при одних и тех же аргументах (!), т.е. no way, учитывая, что haskell чистый. Всё падающее и недетерменированное — в IO, пожалуйста. Всё что имеет семантику изменяемого состояния — в State.

В данном случае вообще видно только создание и разовое изменение баланса, плюс возможность изменять значение в алоцированном замыкании (то самое «ООП» на замыканиях). В хаскеле нет замыканий, создание и изменение баланса вполне имеют право быть чистыми функциями:

type Balance = Integer
type Amount  = Integer

makeBalance :: Integer -> Maybe Balance
makeBalance n
  | n < 0     = Nothing
  | otherwise = Just n

changeBalance :: Amount -> Balance -> Maybe Balance
changeBalance amount balance
  | balance >= amount = Just $ balance - amount
  | otherwise         = Nothing

test = makeBalance 100 
   >>= changeBalance 5
   >>= changeBalance 20
   >>= changeBalance (- 50)

внутрь IO (читай real world транзакции) или State они могут попасть уже позже. Ну и типы Balance и Amount могут быть другими, например, вместо Balance и Maybe Balance может быть:

data BalanceOf a 
  = DeadBalance
  | GoodBalance a
  deriving ( Show, Read, Eq, Ord {- , Num, Functor, Applicative, Monad -} )

type Amount  = Integer
type Balance = BalanceOf Amount

makeBalance :: Amount -> Balance
makeBalance n
  | n < 0     = DeadBalance
  | otherwise = GoodBalance n

changeBalance :: Amount -> Balance -> Balance
changeBalance amount DeadBalance           = DeadBalance
changeBalance amount (GoodBalance balance)
  | balance >= amount                      = GoodBalance $ balance - amount
  | otherwise                              = DeadBalance

test = makeBalance 100 
    <> changeBalance 5 
    <> changeBalance 20 
    <> changeBalance (- 50)

infixl 0 <>
(<>) = flip ($)

(тут изменяемое значение просто тащится через аппликации, без всяких монад).

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

То что у IO или у State есть монадический интерфейс это уже вторично (хотя и удобно).

quasimoto ★★★★
()

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

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

забыл учесть, что нужно учесть что денег на счету может не хватать, тогда будет такой вид:

[code] import Data.Either

withdraw :: [Either Int Int] withdraw = (Right 100) : (zipWith (either act act) withdraw acts) acts = [5, 20, -50, 300, -10]

act balance amount | (balance >= amount) = Right (balance - amount) | otherwise = Left balance

[/code]

а вывод такой:

[code] [Right 100, Right 95, Right 75, Right 125, Left 125, Right 135] [/code]

где Left - обозначает что деньги сняты

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

> Прямым аналогом была бы функция которая имеет разные значения при одних и тех же аргументах

Ну я и говорю - написать аналог на хаскеле нельзя.

создание и изменение баланса вполне имеют право быть чистыми функциями:

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

То что у IO или у State есть монадический интерфейс это уже вторично (хотя и удобно).

Да, признаю, что выразился не совсем корректно. Под монадическими костылями подразумевался факт протаскивания значения сквозь все вычисления. С монадами или без - не так важно. В варианте ОПа мы можем спокойно писать:

[code] (new-withdraw 10) (new-withdraw 20) (new-withdraw 30) [/code] и все будет работать. В хаскеле мы в любом случае вынуждены писать: [code] (new-withdraw 30 (new-withdraw 20 (new-withdraw 10 100))) [/code] (вместо обычной аппликации может быть любой ее заменитель типа того же bind'a) То есть мы можем реализовать только вот такую функцию: [code] (define (new-withdraw amount balance) (if (>= balance amount) (begin (set! balance (- balance amount)) balance) «Недостаточно денег на счете»)) [/code] И это _не та_ функция, что просил ОП. Мы, конечно, можем «скрыть» явную передачу состояния за счет того самого монадического интерфейса, но остается одна проблема - чтобы синхронизировать это состояние в разных местах нам придется изменять тип всех ф-й, в которых используется new-withdraw и пихать, снова, те же монады или явно передаваемое состояние. И если мы используем эти новые ф-и в каких-то других - они опять будут наследовать этот недостаток. В результате код будет очень значительно усложняться.

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

> Прямым аналогом была бы функция которая имеет разные значения при одних и тех же аргументах

Ну я и говорю - написать аналог на хаскеле нельзя.

создание и изменение баланса вполне имеют право быть чистыми функциями:

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

То что у IO или у State есть монадический интерфейс это уже вторично (хотя и удобно).

Да, признаю, что выразился не совсем корректно. Под монадическими костылями подразумевался факт протаскивания значения сквозь все вычисления. С монадами или без - не так важно. В варианте ОПа мы можем спокойно писать:

[code] (new-withdraw 10) (new-withdraw 20) (new-withdraw 30) [/code] и все будет работать. В хаскеле мы в любом случае вынуждены писать: [code] (new-withdraw 30 (new-withdraw 20 (new-withdraw 10 100))) [/code] (вместо обычной аппликации может быть любой ее заменитель типа того же bind'a) То есть мы можем реализовать только вот такую функцию: [code] (define (new-withdraw amount balance) (if (>= balance amount) (begin (set! balance (- balance amount)) balance) «Недостаточно денег на счете»)) [/code] И это _не та_ функция, что просил ОП. Мы, конечно, можем «скрыть» явную передачу состояния за счет того самого монадического интерфейса, но остается одна проблема - чтобы синхронизировать это состояние в разных местах нам придется изменять тип всех ф-й, в которых используется new-withdraw и пихать, снова, те же монады или явно передаваемое состояние. И если мы используем эти новые ф-и в каких-то других - они опять будут наследовать этот недостаток. В результате код будет очень значительно усложняться.

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

Ну я и говорю - написать аналог на хаскеле нельзя.

Угу, вообще, при компиляции лямбда-исчисления есть только два варианта — либо аллоцировать замыкания, либо полностью устранять лямбды и заменять их суперкомбинаторами. Для хаскеля это первоначальная raison d'être — чтобы все функции были чистыми.

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

Какие? liftM* и fmap (обе — (a -> b) -> f a -> f b) умеют погружать любое чистое вычисление в IO, который имеет kind * -> * и семантически разделяет вычисления на детерминированные и индетерминированные (ну или pure и effectful). do нотация это тоже способ осуществлять чистые вычисления внутри IO. А сами чистые функции как работали так и будут работать, просто на разных уровнях.

На самом деле, учитывая тип main, все чистые функции после компиляции итак оказываются внутри IO.

(new-withdraw 10) (new-withdraw 20) (new-withdraw 30)

Это вещи тоже не в вакууме существуют, их выполняет рантайм у которого есть поддержка мутабельности. У GHC-ного рантайма тоже такая есть, выше были примеры (на тему MVar, IORef) — будет тоже самое что и в scheme, но, естественно, внутри IO :) Иначе просто никак.

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

Мы, конечно, можем «скрыть» явную передачу состояния за счет того самого монадического интерфейса, но остается одна проблема - чтобы синхронизировать это состояние в разных местах нам придется изменять тип всех ф-й, в которых используется new-withdraw

Моё ИМХО тут в том, что пример OPа спокойно пишется чистыми функциями, т.е. монада IO вообще не возникает. Если функцию можно сделать чистой, то её нужно сделать чистой, при этом большинство кода который пишется это чистый код, на долю IO/STM/... остаются только вещи масштаба всего приложения — интерфейс, масштабирование/конкурентность, in-place обработка больших данных и т.п.

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

> Моё ИМХО тут в том, что пример OPа спокойно пишется чистыми функциями, т.е. монада IO вообще не возникает.

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

(new-withdraw 30 (new-withdraw 20 (new-withdraw 10 100)))

Теперь _любая_ функция, которая вызывает new-withdraw необходимо сама будет протаскивать состояние (и получать и возвращать). И, собственно, в хаскеле есть куча костылей, которые предназначены только для того, чтобы избавить программиста от протаскивания этих состояний руками (но они все ранво не избавляют, хотя и упрощают это дело). В варианте на схеме состояние надежно инкапсулировано в самой функции, его не приходится размазывать по всей программе. Фишка в том, что на схеме можно написать, как на хаскеле. А вот наоборот - никак.

Какие? liftM* и fmap (обе — (a -> b) -> f a -> f b) умеют погружать любое чистое вычисление

Ну вот в том и суть. У нас был нормальный код - а станет мешанина из лифтов, биндов, do-нотации и прочего говна.

anonymous
()

Говорят, бабло лучше считать на Java :)

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

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

И это будет место уже в top-level программы.

Теперь _любая_ функция, которая вызывает new-withdraw необходимо сама будет протаскивать состояние (и получать и возвращать).

Последние ~1K LOC что я писал на хаскеле были программой, которая выгребает tag soup из веба с целью получения каких-то данных, т.е. там должно быть много IO, тем не менее IO (точнее IO Maybe) там только в нескольких методах класса который используется для представления интерфейса к разным бакендам, все do/lift/fork/binary костыли - в инстансах этого класса, а весь остальный код работающий с tag soup, строящий правильные структуры данных и их проверяющий, т.е. большая часть кода, - чистый код. Ну и сами do-костыли довольно естественно выглядят.

У нас был нормальный код - а станет мешанина из лифтов, биндов, do-нотации и прочего говна.

почти (с) я сам :3

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

Наглое враньё.

Да ничего подобного. Повторен функционал вот такой вот функции:

(define (new-withdraw amount balance)
  (if (>= balance amount)
      (begin (set! balance (- balance amount))
             balance)
      "Недостаточно денег на счете"))

И это, очевидно, не функция ОПа.

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

> И это будет место уже в top-level программы.

Это будет _везде_, где вызывается наша функция. Или функция, которая вызывает нашу функцию. И так далее.

Последние ~1K LOC что я писал на хаскеле были программой, которая выгребает tag soup из веба

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

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

Ну я и говорю - написать аналог на хаскеле нельзя.

А я говорю, что можно, и при этом привожу код, кстати, чуть выше (который под лозунгом «императивщина — значит IO»).

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

В этом и прелесть чистых функций.

Под монадическими костылями подразумевался факт протаскивания значения сквозь все вычисления. С монадами или без - не так важно.

Виндузятник, ты ли это?

В варианте ОПа мы можем спокойно писать:

(new-withdraw 10)
(new-withdraw 20)
(new-withdraw 30)
и все будет работать.

new-withdraw принимает нуль аргументов. Ты что, схемы не знаешь? Ладно, предположим, что ты имел в виду

((new-withdraw) 10)
((new-withdraw) 20)
((new-withdraw) 30)

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

Мы, конечно, можем «скрыть» явную передачу состояния за счет того самого монадического интерфейса, но остается одна проблема - чтобы синхронизировать это состояние в разных местах нам придется изменять тип всех ф-й, в которых используется new-withdraw и пихать, снова, те же монады или явно передаваемое состояние. И если мы используем эти новые ф-и в каких-то других - они опять будут наследовать этот недостаток. В результате код будет очень значительно усложняться.

Ничего не будет усложняться. С какого фига вообще? Эвристики в стиле «этот код будет в монадах, а значит будет усложняться» не работают в моем случае.

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

> Да ничего подобного. Повторен функционал вот такой вот функции:

Да нет же, посмотри на начало топика, я там специально для зануд привел дословную трансляцию схемовского кода.

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

А я говорю, что можно, и при этом привожу код, кстати, чуть выше (который под лозунгом «императивщина — значит IO»).

Ну так тот код, что ты привел - аналогом не является. Он не делает того, что надо ОПу, твой код - аналог функции, которую я написал в прошлом посте (с явной передачей баланса).

В этом и прелесть чистых функций.

А задача ОПа требует отсутствия этих ограничений.

new-withdraw принимает нуль аргументов.

Каюсь. Нужно было так:

(define withdraw (new-withdraw))
(withdraw 10)
(withdraw 20)
(withdraw 30)

Ничего не будет усложняться. С какого фига вообще?

С того, что был обычный код, а станет невнятное говно с do-нотацией и лифтами.

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

А, чорт, это я схемы не знаю. new-withdraw именно так и надо использовать. Получается, что бананс — синглтон чтоли?

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

> Ну так тот код, что ты привел - аналогом не является. Он не делает того, что надо ОПу, твой код - аналог функции, которую я написал в прошлом посте (с явной передачей баланса).

А ты пробовал его использовать? Дай-ка покажу.

[code]*Main> f <- newWithdraw *Main> f 10 90 *Main> f 20 70 *Main> f 80 *** Exception: Недостаточно денег на счете[/code]

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

Ну так тот код, что ты привел - аналогом не является. Он не делает того, что надо ОПу, твой код - аналог функции, которую я написал в прошлом посте (с явной передачей баланса).

А ты пробовал его использовать? Дай-ка покажу.

*Main> f <- newWithdraw
*Main> f 10
90
*Main> f 20
70
*Main> f 80
*** Exception: Недостаточно денег на счете
anonymous
()
Ответ на: комментарий от anonymous

> Да не, ты правильно написал сначала.

Ага, совсем ты меня запутал. Там же скобок не было при определении new-withdraw, а я и не проверил.

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

что просит ОП

Есть из коробки.

инкапсулировать некое состояние

= IO или STM.

и предоставить удобный интерфейс для работы с ним.

= монадический интерфейс.

Любое состояние придется размазывать по всей программе.

Наоборот - «программа» это и есть effectful работа с состояния, эти самые do и т.п. Это не IO размазывается по чистым функциям, это чистые функции используются внутри блоков IO. По крайней мере, такой дизайн предполагается.

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

А, чорт, это я схемы не знаю. new-withdraw именно так и надо использовать. Получается, что бананс — синглтон чтоли?

Обычная функция. Считай, что оно эквивалентно следующему:

(define balance 100)
(define (new-withdraw amount)
  (if (>= balance amount)
      (begin (set! balance (- balance amount))
             balance)
      "Недостаточно денег на счете"))
только к balance нету доступа из других ф-й. В сущности, того же эффекта можно добиться обычным макросом, который раскроется в вышеприведенный код:
(define-syntax-rule (withdraw-macro name)
  (begin (define balance 100)
         (define (name amount)
           (if (>= balance amount)
               (begin (set! balance (- balance amount))
                      balance)
               "Недостаточно денег на счете"))))

(withdraw-macro new-withdraw)

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

> А ты пробовал его использовать? Дай-ка покажу.

Угу, только чего ж ты в репле вызываешь? Давай по-честному.

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

> Есть из коробки.

Где?

= IO или STM.

IO и STM как раз не инкапсулируют состояние. Состояние остается размазанным, но указанные монады позволяют снизить затраты на передачу состояния ручками (состояние передается само внутри бинда). Никакой возможности инкапсулировать состояние в хаскеле нет - потому что грязных ф-й нет.

По крайней мере, такой дизайн предполагается.

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

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

> Ну да, предполагается, но не все задачи (хотя, по крайней мере, большинство) на этот дизайн ложатся. Вот пример ОПа - одно из таких исключений.

Поясню, почему. классический вариант - мы что-то ввели, потом обработали, потом вывели. Логика программы заключается именно в той части, которая занимается обработкой. Это 99% программы и за обработку отвечают чистые функции. Нам остается только «состыковать» эти чистые функции с грязным вводом-выводом тоненькими прослойками - и вуаля! С примером ОПа же у нас как раз логика выходит грязной. В результате у нас вместо «грязный ввод - чистая обработка - грязный вывод» вводится некое грязное состояние именно в ту часть, которая предполагается чистой, и дает непредсказуемые метастазы по всей этой части. И оно уже не будет отделяться от чистого кода так хорошо, как логика обработки от ввода/вывода - просто потому, что оно само есть часть логики.

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

IO и STM как раз не инкапсулируют состояние.

Да там (IORef, MVar, TVar, ...) захардкожено именно это - в памяти висит изменяемая область памяти, с ней умеет работать рантайм (в т.ч. GC), ничего нигде не размазывается. Т.е. ни IO ни STM переменные средствами хаскеля (чисто) не делаются.

Вот пример ОПа - одно из таких исключений.

То пример-демонстрация работы замыканий, это не задача (дано это - полить то).

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

> Да там (IORef, MVar, TVar, ...) захардкожено именно это - в памяти висит изменяемая область памяти, с ней умеет работать рантайм (в т.ч. GC), ничего нигде не размазывается. Т.е. ни IO ни STM переменные средствами хаскеля (чисто) не делаются.

Проблема в том, что все это происходит внутри монады. А монадические вычисления мы умеем связывать только биндом, так?

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

> Угу, только чего ж ты в репле вызываешь? Давай по-честному.

Да, а ты думал, что сикповский код запускают не в репле, когда пишут примеры использования? :)

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

что оно эквивалентно следующему

И тут уже не далеко и до =-

Но пропала возможность вызвать несколько раз new-withdraw и менять разные балансы независимо (var из замыкания должен заменяться на var_unique_id).

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

> Но пропала возможность вызвать несколько раз new-withdraw и менять разные балансы независимо (var из замыкания должен заменяться на var_unique_id).

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

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