Понимание типа IO() в выражении `let`

Дано:

λ: let f = putStrLn "foo" in 42
42

Что такое тип f? Почему "foo" не распечатывается перед показом результата 42?

Наконец, почему не работает следующее?

λ: :t f

<interactive>:1:1: Not in scope: ‘f’

Ответ 1

Что такое f type?

Как вы правильно определили, это IO (), которое можно рассматривать как действие IO, которое ничего не возвращает ничего полезного (())

Почему "foo" не печатается перед показом результата 42?

Haskell лениво оценивается, но даже seq в этом случае недостаточно. Действие IO будет выполняться только в REPL, если выражение возвращает действие IO. Действие IO будет выполняться только в программе, если оно возвращается main. Однако есть способы обойти это ограничение.

Наконец, почему не работает следующее?

Haskell let называет значение в пределах области выражения, поэтому после оценки выражения f выходит за рамки.

Ответ 2

let f = ... просто определяет f и ничего не запускает. Это смутно похоже на определение новой функции в императивном программировании.

Ваш полный код let f = putStrLn "foo" in 42 можно свободно перевести на

{
  function f() {
     print("foo");
  }
  return 42;
}

Вы не ожидали, что вышеприведенный текст ничего не напечатает, верно?

Для сравнения, let f = putStrLn "foo" in do f; f; return 42 похож на

{
  function f() {
     print("foo");
  }
  f();
  f();
  return 42;
}

Соответствие не совершенное, но, надеюсь, вы получите эту идею.

Ответ 3

f будет иметь тип IO ().

"foo" не печатается, потому что f не привязан к реальному миру. (Я не могу сказать, что это дружеское объяснение. Если это звучит глупо, вам может понадобиться отнестись к некоторому учебнику, чтобы поймать идею Монады и ленивую оценку).

let name = value in (scope) делает значение доступным, но не вне области видимости, поэтому :t не найдет его в области верхнего уровня ghci.

let без in делает его доступным для :t (этот код действителен только в ghci):

> let f = putStrLn "foo"
> :t f
f :: IO ()

Ответ 4

Здесь есть две вещи.

Сначала рассмотрим

let x = sum [1..1000000] in 42

Хаскелл ленив. Поскольку мы ничего не делаем с помощью x, он никогда не вычисляется. (Это так же хорошо, потому что это будет мягко медленно.) Действительно, если вы скомпилируете это, компилятор увидит, что x никогда не используется и не удаляет его (т.е. Не сгенерировать скомпилированный код для него).

Во-вторых, вызов putStrLn фактически ничего не печатает. Скорее, он возвращает IO (), который вы можете рассматривать как своего рода "объект команды ввода-вывода". Просто наличие объекта команды отличается от его выполнения. По дизайну единственный способ "выполнить" объект команды ввода-вывода - вернуть его из main. По крайней мере, это полная программа; GHCi имеет полезную функцию: если вы введете выражение, которое возвращает объект команды ввода-вывода, GHCi выполнит его для вас.

Ваше выражение возвращает 42; снова, f не используется, поэтому ничего не делает.

Как chi справедливо указывает, он немного напоминает объявление локальной (нулевой аргумент) функции, но никогда не вызывает ее. Вы не ожидаете увидеть какой-либо вывод.

Вы также можете сделать что-то вроде

actions = [print 5, print 6, print 7, print 8]

Создает список объектов команды ввода-вывода. Но, опять же, он не выполняет ни одного из них.

Обычно, когда вы пишете функцию, которая делает I/O, это блок блокировки, который объединяет все в один гигантский объект команды ввода-вывода и возвращает его вызывающему. В этом случае вам не нужно разбираться в этом различии между определением объекта команды и ее выполнением. Но различие все еще существует.

Возможно, это проще увидеть с помощью монады с явной функцией запуска. Например, runST принимает объект команды ST, запускает его и возвращает ответ. Но (скажем) newSTVar сам по себе ничего не делает, кроме как построить команду ST; вы должны runST, прежде чем что-либо на самом деле "произойдет".