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?

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

> А с этим есть какие-то проблемы?

Да, и это хорошо. Можно, конечно, призвать unsafePerformIO и сделать глобальную мутабельную переменную. Но зачем?

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

> Да, и это хорошо.

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

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

Этот ваш Кэп занимается подменой понятий и демагогией.

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

При чем тут возможность закешировать результат?

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

А с этим есть какие-то проблемы?

Да так. Плохая практика.

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

> При том. Нам придётся учитывать, что функция нечистая. В любом языке. В том числе - потому что мы не вправе кэшировать её результат.

Что за бред? В нормальных языках (без разделения на чистые/грязные ф-и) любая функция - грязная, и учитывать ничего не надо.

Да так. Плохая практика.

Тогда почему все, кто пишет на хаскеле, всегда этой практикой пользуются? Я вижу тут явное противоречие. «плохая практика» - это ведь по определению то, чего следует избегать.

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

В нормальных языках (без разделения на чистые/грязные ф-и) любая функция - грязная, и учитывать ничего не надо.

То есть, запоминать результат любой функции для последующего использования в нескольких местах - дело априори бессмысленное?

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

Да ну?

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

> То есть, запоминать результат любой функции для последующего использования в нескольких местах - дело априори бессмысленное?

Конечно.

Да ну?

Ну да. Попробуй найти хоть одну программу на хаскеле, где не используется глобальных переменных. Не выйдет. По крайней мере будет main. На и еще bind/return etc. что там в стандартной библиотеке.

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

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

Пожалуй, ты прав. В простых случаях, когда только один вариант ошибки, действительно можно спокойно пользоваться Maybe. А в сложных возвращать именно String лишено здарового смысла. Потому что, например, паттернматчить потом этот String — не самое большое удовольствие.

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

Когда я отучусь нажимать «поместить» раньше времени?

По крайней мере будет main.

Не, не пойдёт. Константы, определяемые на этапе компиляции — фиг с ними. Глобальные переменные — это немножко другое.

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

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

На hackage полно пакетов (не программы, конечно, но библиотеки) которые вообще не используют IO.

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

Ну и ещё нужно дать определение глобальной переменной в хаскеле - это CAF с типом IO (SomeReference a) или STM (SomeReference a). main под такое определение не попадает.

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

> Гм. То есть, ряд фибоначчи быстрее экспоненты не считается?

Почему не считается? Считается.

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

> На hackage полно пакетов (не программы, конечно, но библиотеки) которые вообще не используют IO.

Как ИО связано с глобальными переменными? Любая определенная внутри пакета ф-я - глобальная переменная.

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

Глобальная переменная - это любая глобальная переменная. Вне зависимости от ее типа. Хоть там IO, хоть что.

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

> Не, не пойдёт. Константы, определяемые на этапе компиляции — фиг с ними. Глобальные переменные — это немножко другое.

А чем они отличаются? Рассмотренная ф-я f - тоже константа, определяемая на этапе компиляции. В хаскеле вообще нету других сущностей - только константы, определяемые на этапе компиляции. И возвращает программа на хаскеле всегда тоже константу, определяемую на этапе компиляции.

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

Как ИО связано с глобальными переменными? Любая определенная внутри пакета ф-я - глобальная переменная.

Это вы применяете неправильную терминологию (из другой области). Любая функция это суперкомбинатор (SC), отличается тем свойством, что для одних и тех же аргументов возвращает одно и то же значение, то есть никакая это не «переменная» (не меняется), а самая настоящая функция в математическом смысле. Далее, SC без аргументов это константная аппликативная форма (CAF) - просто константа, которая _всегда_ имеет одно и то же значение и _никогда_ не может измениться. И, наконец последнее, люди что делают GHC добавляют SCs и CAFs специального вида с типами (... -> IO/STM (SomeRef a)) и IO/STM (SomeRef a) соответственно, и эти формы могут давать разные значения при одних и тех же условиях, то есть являются, буквально, «переменными» - могут меняться со временем, но только внутри (охрана такая, чтобы они не смешивались с обычными SCs и СAFs) IO или STM.

Естественно, всё это даётся не простой ценой - IO и STM вместе с мутабельными referenc-ами (в т.ч. массивами, обычный референс можно считать одноэлементным массивом) требуют дописывания рантайма (шедулера, аллокатора, gc).

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

В хаскеле вообще нету других сущностей - только константы

Дык переменные или константы?) (или это разные анонимусы?)

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

> Это вы применяете неправильную терминологию (из другой области). Любая функция это суперкомбинатор (SC), отличается тем свойством, что для одних и тех же аргументов возвращает одно и то же значение, то есть никакая это не «переменная» (не меняется), а самая настоящая функция в математическом смысле.

Ну так в хаскеле тогда вообще нету переменных. И f - обычная функция, которая каждый раз возвращает одно и то же значение. В хаскеле вообще нету способа написать функцию, которая бы возвращала разные значения при разных вызовах (если только не использовать unsafePerformIO, но речь сейчас не о нем).

и эти формы могут давать разные значения при одних и тех же условиях

Не могут, конечно же.

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

> Дык переменные или константы?) (или это разные анонимусы?)

Формально, константа - частный случай переменной (переменная, которая может принимать только одно значение).

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

Почему не считается? Считается.

Но как же? Для этого же надо — ужас какой — запоминать результат предыдущего вычисления! Это ж нельзя делать!

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

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

Вот ведь бред какой у некоторых в голове.

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

В хаскеле вообще нету способа написать функцию, которая бы возвращала разные значения при разных вызовах

> let rollDice = getStdRandom (randomR (1,6)) :: IO Int
> rollDice
3
> rollDice
1
> rollDice
5

как тогда это интерпретировать?

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

> Но как же? Для этого же надо — ужас какой — запоминать результат предыдущего вычисления! Это ж нельзя делать!

Во-первых, для чего их надо запоминать? Во-вторых, почему нельзя?

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

> как тогда это интерпретировать?

rollDice - константа с типом IO Int. Репл же ваш выводит не IO Int, а Int, который получается, например, как unsafePerformIO rollDice. Ну а unsafePerformIO, как известно, не функция (в терминологии хаскеля), так что нет ничего странного в том, что она возвращает такой вот странный результат (разный результат при применении к одному и тому же аргументу).

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

Ну понятно. Давайте лучше про операционные семантики IO поговорим.

Если считать, что чистый хаскель это разновидность HM или SystemF, то тут всё просто - их операционная семантика вопросов не вызывает. Теперь добавим сюда индуктивные типы данных - с ними тоже проблем нет. И сделаем такое построение:

{-# LANGUAGE ExistentialQuantification #-}

import Prelude hiding ( IO, putChar, getChar, putStr, putStrLn, getLine )
import Data.Default
import Control.Applicative
import Control.Monad

data IO a
  = Return a
  | forall t. Put t (IO a)
  | forall t. Default t => Get (t -> IO a)    -- Default нужен только чтобы написать нормальный instance Show.

instance Show a => Show (IO a) where
  show (Return x) = show x
  show (Put _ x)  = "Put\n" ++ show x
  show (Get g)    = "Get\n" ++ show (g def)

instance Functor IO where
  fmap f (Return x) = Return $ f x
  fmap f (Put z x)  = Put z $ fmap f x
  fmap f (Get g)    = Get $ \z -> fmap f $ g z
  
instance Applicative IO where
  pure  = return
  (<*>) = ap

instance Monad IO where
  return         = Return
  Return x >>= f = f x
  Put z x  >>= f = Put z $ x >>= f
  Get g    >>= f = Get $ \z -> g z >>= f

putChar :: Char -> IO ()
putChar x = Put x $ return ()

getChar :: IO Char
getChar = Get return

putStr :: String -> IO ()
putStr = mapM_ putChar

putStrLn :: String -> IO ()
putStrLn xs = putStr xs >> putChar '\n'

getLine :: IO String
getLine = do
  c <- getChar
  if c == '\n'
  then return [c]
  else fmap (c :) getLine

getNLine :: Int -> IO String
getNLine n
  | n <= 0    = return ""
  | otherwise = do
    c <- getChar 
    fmap (c :) $ getNLine (n - 1)

main :: IO ()
main = do
  name <- getNLine 5
  putStrLn $ "Hi, " ++ name

instance Default Char where
  def = '\0'

всё написаное не привлекает никаких хаков, и может быть выражено в HM или SystemF с индуктивными типами (т.е. в системе с вполне нормальной операционной семантикой). Теперь, что есть main с точки зрения её редукций:

> main
Get
Get
Get
Get
Get              -- (getNLine 5) читает 5 знаков
Put
Put
Put
Put
Put
Put
Put
Put
Put
Put              -- putStrLn печатает 5 + 4 + 1 знаков
()

т.е. это ассемблер некой VM который может выполнить рантайм. Теперь видно, что всё что осталось - добавить к операционной семантике правила для конструкторов IO:

[[Return x]] = |
[[Put z x]]  = | архитектурная хрень
[[Get g]]    = |

подробнее про это можно почитать в An Operational Semantics for I/O in a Lazy Functional Language и в публикациях Wouter Swierstra про distributed arrays.

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

> Ну понятно. Давайте лучше про операционные семантики IO поговорим.

А зачем? Какое отношение это имеет к хаскелю? Вам программа на хаскеле вернула некоторое вычисление. Это константа. Как это вычисление будет представлено (например - ассемблер некоей VM) и как он будет выполняться - это уже дело десятое и к хаскелю никакого отношения не имеет. В данном случае мы уже находимся в рамках совершенно другого ЯП (того самого ассемблера).

Если считать, что чистый хаскель это разновидность HM или SystemF

Ну а зачем? Можно взять семантику простой нетипизированной лямбды.

всё написаное не привлекает никаких хаков, и может быть выражено в HM или SystemF с индуктивными типами (т.е. в системе с вполне нормальной операционной семантикой).

Это все, конечно, хорошо, но функцию Get вы не можете написать, не привлекая хаков. Вы ее даже типизировать в рамках хаскеля не можете - это вам не Clean, SystemF чересчур бедна. По-этому никакой такой ф-и Get в хаскеле просто _нету_. И быть не может.

Но разговор как-то в сторону ушел. Вопросы семантики ИО - это одно, а тот факт, что в хаскеле есть только константы - совсем другое.

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

Это константа.

SC или CAF? Ну да, константа, просто не нужно забывать, что кроме чистой HM/SystemF есть ещё и рантайм который эту константу вычисляет (причём, по вполне определённым правилам). Если мы берём за «хаскель» всё что описано в report, в том числе IO, то нам нужно брать как чистую HM/SystemF, так и низлежащий рантайм.

Как это вычисление будет представлено (например - ассемблер некоей VM) и как он будет выполняться - это уже дело десятое и к хаскелю никакого отношения не имеет.

Отношение это имеет такое, что если мы хотим дать операционную семантику хаскеля, то нам нужны как source (т.е. сам рассахаренный хаскель), так и target (какая-то VM). Например, как в «From System F to Typed Assembly Language» - source это System F, конечный target - TAL.

Ну а зачем? Можно взять семантику простой нетипизированной лямбды.

Ну мы про хаскель говорим? Значит нужно брать по крайней мере HM.

но функцию Get вы не можете написать

А что я выше тогда написал? Или вы имеете ввиду, что нельзя написать (IO a) -> VM? Но ведь можно, вплоть до (IO a) -> VM -> MachCode.

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

> А что я выше тогда написал?

Ну никакой ф-и вы выше не написали, в написали только некоторый тип. Причем это не тип ф-и Get (если под Get понимать функцию, которая производит вычисление IO). То есть если бы вы все же сумели написать такую ф-ю, и она имела бы именно тип IO a -> a, то у вас корректно типизированные программы давали бы непредсказуемые ошибки control-flow в рантайме. Как это и происходит с unsafePerformIO (она, фактически, и есть ваша Get).

Ну мы про хаскель говорим? Значит нужно брать по крайней мере HM.

Зачем? Хаскель заканчивается ровно в том месте, где мы получили наш main. Как мы уже выяснили, можно считать main программой на некотором ЯП ассемблера vm. Стандарт не накладывает на ЯП ассемблера никаких ограничений - он может быть любым, связь с хаскелем не обязательна.

Отношение это имеет такое, что если мы хотим дать операционную семантику хаскеля, то нам нужны как source (т.е. сам рассахаренный хаскель), так и target (какая-то VM). Например, как в «From System F to Typed Assembly Language» - source это System F, конечный target - TAL.

Операционная семантика хаскеля заканчивается на том, как мы вычислили IO (то есть процесс компиляции). Как потом рантайм будет IO исполнять - это уже не хаскель.

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

Ну никакой ф-и вы выше не написали, в написали только некоторый тип.

Конструктор терма IO a:

Get :: (t -> IO a) -> IO a

конструктор это тоже функция.

Как это и происходит с unsafePerformIO (она, фактически, и есть ваша Get).

Нет, unsafePerformIO это совсем другое - это run для IO:

class Run t where
  run :: t a -> a

instance Run IO where
  run (Return x) = x
  run (Put _ x)  = run x
  run (Get g)    = run $ g def

> run $ getNLine 5
"\NUL\NUL\NUL\NUL\NUL"

в рантайме это будет не пять '\0', а настоящая строчка.

у вас корректно типизированные программы давали бы непредсказуемые ошибки control-flow в рантайме.

В динамически типизированных языках это происходит постоянно, в них просто безопасный рантайм (с чем-то вроде рестартов и проверок тайптегов времени выполнения). Хаскель просто запрещает такое поведение, или, как GHC, разрешает, но падает в проблемных местах (хотя тоже мог бы быть безопасным).

Операционная семантика хаскеля заканчивается на том, как мы вычислили IO (то есть процесс компиляции). Как потом рантайм будет IO исполнять - это уже не хаскель.

Вы не до конца поняли приведённый пример. Тут (IO a) это некий язык с конструкторами термов Return, Get, Put, ещё бы следовало добавить конструкторы New (как у Wouter Swierstra), Delete и Set. При этом в операционную семантику входят и правила преобразования термов IO. Т.е., например, если хаскель вместе с термами IO это source, а си это target, то в операционную семантику будут входить правила (грубо говоря):

[[Put ...]] = write ...
[[Get ...]] = read ...
[[New ...]] = malloc
[[Delete ...]] = free
[[Set ...]] = `='

Т.е. после процесса компиляции (выполнения правил операционной семантики) никакого IO вообще нет, есть только термы target (тут - си).

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

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

>Как ИО связано с глобальными переменными? Любая определенная внутри пакета ф-я - глобальная переменная.

Глобальная - может быть, но не переменная =)

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

>Формально, константа - частный случай переменной (переменная, которая может принимать только одно значение).

В такой формулировке в хаскеле констант нету.

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

> Конструктор терма IO a:

Тьфу ты, невнимательно тип посмотрел.

Нет, unsafePerformIO это совсем другое - это run для IO:

Да, да. Все, что я говорил про Get - это надо было сказать про run, которой полностью семантика IO и определяется.

В динамически типизированных языках это происходит постоянно

Вообще никогда не происходит. Порядок-то аппликативный. А при аппликативном порядке таких ошибок быть не может.

Хаскель просто запрещает такое поведение

Не запрещает и запретить не может. Система типов слишком бедная, чтобы это запретить.

Т.е. после процесса компиляции (выполнения правил операционной семантики) никакого IO вообще нет, есть только термы target (тут - си).

Ну IO - это и есть терм target в данном случае.

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

Нет, в Clean ситуация другая. В Clean система типов на порядок мощнее, чем в хаскеле, за счет этого можно корректно типизировать run. То есть run - часть языка, в отличии от хаскеля. Программа на хаскеле возвращает IO, причем с точки хаскеля мы не можем ничего об этом IO сказать, «внутренность» этого IO - уже не хаскель. В Clean ситуация обратная, внутренность IO - тоже Clean. Другими словами тут два разных подхода. В хаскеле мы просто отделяем всю «грязь» и говорим «что будет потом сделано с этой грязью - неизвестно, это не проблема хаскеля». В Clean же предоставляются методы для управления этой «грязью», и мы с ней можем обращаться при помощи стандартных языковых средств.

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

> main - не константа

А что же это, как не константа с типом IO ()?

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

> Уговорил, там написано что «бессмысленно».

Нет, ключевой момент в том, что там было написано «для любой». Для любой - действительно, бессмысленно. А вот для какой-то конкретной - может, смысл и имеет.

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

То есть нет, я спрошу по-другому. Если main - не константа, то она может принимать разные значения при разных запусках программы, так?

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

А вот для какой-то конкретной - может, смысл и имеет.

Так. Отлично. То есть, ты согласен, что деление на чистые и нечистые функции вполне практично?

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

Ну IO - это и есть терм target в данном случае.

Да я твержу-твержу, что нет. Термы IO переводятся в термы target, также как и любые другие хаскельные термы. Т.е. можно дать семантику в которой IO - target, а я показал пример семантики где IO - часть source, а target - какая-нибудь императивщена с системными вызовами.

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

Uniqueness типы сделаны частью грамматики и правила TI для них отдельные (что-то в духе линейных типов, наверное)? Какие конкретные преимущества это несёт по сравнению с хаскелем? Что делать с STM - оно выражается в uniqueness типах, или в грамматику core нужно добавлять ещё что-то?

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

это надо было сказать про run, которой полностью семантика IO и определяется.

А, собственно, зачем нам run для IO? Любую хаскельную программу можно написать и без него. Факт того, что нельзя в языке определить функцию с типом (IO a -> a) и с семантикой аккессора для IO, это как факт невозможности написать линейную лямбду вида (λ !x . (x, x)), в первом случае это просто ad-hoc запрет, а во втором - действия правил типизации.

Ну и ещё раз скажу, что хаскельная программа это детерминированный Тьюринг-полный вычислитель (просто вариант LC) управляющий недетерминированными потоками данных - эти недетерминированные значения на уровне детерминированного вычислителя появляться (run) и не должны.

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

А, собственно, зачем нам run для IO?

Могу придумать только один вариант - делать IO a -> a для foreign функций про которые известно, что они чистые. Наверно поэтому unsafePerformIO определена в FFI части haskell report.

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

> Так. Отлично. То есть, ты согласен, что деление на чистые и нечистые функции вполне практично?

Где шла речь о чистоте?

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