Простой счетчик в IO - PullRequest
       41

Простой счетчик в IO

2 голосов
/ 20 октября 2011

Я пытаюсь создать простой счетчик, который увеличивается на 1 бесконечно, используя IO.

С тех пор я ломаю голову ...

В идеале, я бы хотелсделать что-то вроде

tick = do putStr (counter)
          counter + 1
    where counter = 0

Затем повторите процесс.Затем повторите первые 2 выражения.Или что-то вроде:

tick = tick'
       where 
           counter = 1
           tick' counter | counter > 0 = do putStrLn (show counter)
                                            tick' (counter + 1)
                         | otherwise = tick

Что дает мне ошибки: /

Любая помощь приветствуется:)

Ответы [ 4 ]

7 голосов
/ 20 октября 2011

Есть несколько способов сделать это без с использованием изменяемой ячейки.Вы уже сделали это со второй попытки, только небольшая ошибка.Вам нужно передать начальное значение в функцию tick', а не «установить его» (haskell не имеет представления о присвоении переменных - только определения. Если появится строка x = y, x будет yза всю свою жизнь).

tick = tick' 0
    where ...

Линия counter = 0 ничего не делает;это определение имени, которое никогда не используется.counter, используемый в функции tick', связан как один из ее аргументов (и скрывает тот, который определен как 0).Потратьте некоторое время, чтобы взглянуть на это с учетом этого, посмотрите, имеет ли это смысл.

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

do
    print 0
    print 1
    print 2
    ...

Существует функция с именем sequence :: [IO a] -> IO [a] (см. Предостережение ниже), которая возьмет список действий и создаст действие.Поэтому, если мы можем построить список [print 0, print 1, print 2, ...], тогда мы можем передать его в sequence, чтобы построить бесконечно длинный блок, который мы ищем.

Обратите внимание, это очень важная концепция в Haskell: [print 0, print 1, print 2] не печатает эти три числа, затем строит список [0,1,2].Вместо этого он представляет собой список действий , тип которых [IO ()].Составление списка ничего не делает;только когда вы привязываете действие к main, оно будет выполнено.Например, мы можем сказать:

main = do
    let xs = [putStrLn "hello", getLine >> putStrLn "world"]
    xs !! 0
    xs !! 0
    xs !! 1
    xs !! 1
    xs !! 0

Это будет дважды печатать hello, дважды получать строку и печатать world после каждого, затем один раз печатать hello снова.

С помощью этой концепции легко создать список действий [print 0, print 1, ...] с пониманием списка:

main = sequence [ print x | x <- [0..] ]

Мы можем немного упростить:

main = sequence (map (\x -> print x) [0..])
main = sequence (map print [0..])

Так что map print [0..] - этосписок действий [print 0, print 1, ...], которые мы искали, затем мы просто передаем это в sequence, который объединяет их в цепочку.

Этот шаблон sequence является общим и имеет свой собственный mapM:

mapM :: (a -> IO b) -> [a] -> IO [b]
mapM f xs = sequence (map f xs)

Таким образом:

main = mapM print [0..]

Примерно так просто, как вы могли захотеть.

Одно замечание о производительности: поскольку мы не используем выходные данные этих функций, мыследует использовать sequence_ и mapM_ с последующим подчеркиванием, которые оптимизированы для этой цели.Обычно это не имеет значения в программе на Haskell из-за сборки мусора, но в данном конкретном случае это своего рода особый случай из-за различных тонкостей.Вы обнаружите, что без _ s использование памяти вашей программой постепенно возрастает, поскольку список результатов (в данном случае [(),(),(),...]) создается, но никогда не используется.

Caveat : я дал сигнатуры типов sequence и mapM, специализированные для IO, а не общей монады, так что читателю не нужно узнавать об ортогональных концепциях действий, имеющих типы и классы типов в одном и том жевремя.

3 голосов
/ 20 октября 2011

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

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

Но скрыть это IORef немного сложно.Тем более, что глобалы плохие.Лучший способ сделать это - создать действие ввода-вывода из другого действия ввода-вывода, а затем закрыть его над IORef.Это даст вам что-то вроде этого:

import Data.IORef

mkCounter :: IO (IO ())
mkCounter = do
    ref <- newIORef 0
    return $ do
        counter <- readIORef ref
        print counter
        writeIORef ref $ counter + 1

Это можно использовать, выполнив что-то вроде этого:

main = do
    tick <- mkCounter
    tick
    tick
    tick
2 голосов
/ 20 октября 2011

Ваша вторая реализация действительно близка!

tick = tick'
       where 
           counter = 1
           tick' counter | counter > 0 = do putStrLn (show counter)
                                            tick' (counter + 1)
                         | otherwise = tick

Давайте рассмотрим ошибки для этого:

Couldn't match expected type `IO b0' with actual type `a0 -> IO b0'
    In the expression: tick'

Давайте добавим несколько типов, чтобы убедиться, что мы получаем то, что хотим.

tick - это действие ввода-вывода.Нам все равно, какое значение инкапсулирует действие, поскольку весь смысл в том, чтобы оно выполнялось вечно.

tick :: IO a

Теперь наша ошибка:

Couldn't match expected type `IO a' with actual type `a0 -> IO b0'
    In the expression: tick'

Ну, это в значительной степенито же самое, никакой помощи там нет.Давайте продолжим.

tick' - это функция, которая принимает некоторое целое число и возвращает действие ввода-вывода, которое печатает целое число и повторяет tick' для следующего значения.Опять же, нам все равно, какое значение инкапсулирует действие, поскольку оно выполняется вечно.

tick' :: Int -> IO b

Подождите, теперь эта ошибка имеет смысл!Мы определили tick = tick', но эти две вещи имеют принципиально разные типы.Один - это действие (tick), один - функция, которая возвращает действие (tick').Все, что нам нужно сделать, это дать tick' некоторое значение, чтобы получить действие, поэтому давайте сделаем это.

Вы пытались сделать это, сказав where counter = 1, но все, что было сделано, это определить counter как1 в утверждении tick = tick', и поскольку counter там не упоминается, он не использовался.

Когда вы сказали tick' counter | ... =, вы не имели в виду тот же counter, что ина линии выше.Там вы определили другую переменную с именем counter, которая была только в области действия в определении tick'.

Так что теперь наш код выглядит так:

tick :: IO a
tick = tick' 1
       where 
           tick' :: Int -> IO b
           tick' counter | counter > 0 = do putStrLn (show counter)
                                            tick' (counter + 1)
                         | otherwise = tick

Если мы попытаемсяскомпилируйте его, ghc не будет жаловаться, и если мы попробуем его в ghci, он будет работать так, как нужно:

% ghci
ghci> :l Tick.hs
Ok, modules loaded: Tick.
ghci> tick
1
2
3
...
25244
^C
Interrupted
ghci>
1 голос
/ 20 октября 2011

Для простого бесконечного счетчика просто используйте рекурсию:

counter n = do print n
               counter (n+1)

main = counter 1

Еще один способ реализовать функциональность tick без использования изменяемого состояния - это смешать монаду State и IO с использованием преобразователей монад:

import Control.Monad.State

type Ticking a = StateT Int IO a

tick :: Ticking ()
tick = do
     modify succ
     get >>= liftIO . print

getCounterValue :: Ticking Int
getCounterValue = get

Затем вы можете использовать его для создания «тикающих» функций IO (с недоразумением: IO здесь необходимо добавить префикс liftIO, поскольку теперь это Ticking a монада, а не IO a):

ticking :: Ticking ()
ticking = do
        liftIO $ putStrLn "Starting"
        tick
        tick
        c <- getCounterValue
        liftIO $ do
            putStrLn ("Finished at " ++ show c)
            putStrLn "Press any Enter to start infinite counter"
            getChar
        forever tick

Который можно преобразовать в «обычный» IO с помощью runStateT (с начальным значением счетчика):

startTicking :: Ticking a -> Int -> IO a
startTicking = evalStateT

Итак:

main :: IO ()
main = startTicking ticking 0
...