Когда требуется строгая эволюция монад?

Учитывая код Haskell:

do  -- it's IO !!!
  ...
  !x <- someFunction
  ... usage of x

насколько здесь полезен !? Это результат монадической (это IO!) оценки, поэтому мои вопросы:

  1. если x является примитивным типом, таким как UTCTime (например, getCurrentTime), Int, Bool - имеет ли ! смысл?

  2. если у x есть «голова», как у String, Map k v, может быть, какая-нибудь запись? Тогда x можно лениться? Насколько ленив? Означает ли это, что когда я использую x позже, происходит настоящее IO?

  3. Имеет ли IO монада какое-то более специфическое поведение в этом контексте? Вроде немного более строгий, чем другие (более «чистые») монады?

По умолчанию ввод-вывод является ленивым, тогда как Haskell узнает, когда его вызвать немедленно (передавая это выражение) или когда заменить его переходником? По типу x? Что-то вроде: GHC имеет список примитивных типов и все, что находится за пределами списка, подставляется?

Любые объяснения приветствуются.

x — это не IO a, у него есть тип a.
willeM_ Van Onsem 27.06.2024 13:23

Для вещей, требующих более глубокой оценки, обычно существуют варианты someFunction с «глубокой последовательностью».

willeM_ Van Onsem 27.06.2024 13:34

@willeM_VanOnsem Я имею в виду, что монада - это IO, а не что-то вроде списка, Maybe или чего-то подобного.

RandomB 27.06.2024 14:17
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
3
104
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Внутри монады IO !x <- ... не совпадает с x <- ... в общем случае.

Например,

do x <- (return undefined :: IO Bool)
   print True

не дает сбоя, поскольку undefined никогда не используется принудительно. Использование !x <- ... вместо этого приведет к принудительному завершению работы и сбою.

Аналогично, если действие IO заканчивается return (f y), где f требуется много времени для вычисления, то x <- myIOaction не сразу вызывает f y (который будет оценен позже, когда и если x потребуется), в то время как !x <- myIOaction заставляет f y.

IO не является чем-то особенным в этом аспекте: после x <- action переменная x может быть связана с полностью вычисленным значением или невычисленным преобразователем, как и в (почти) любой другой монаде.

Ответ принят как подходящий

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

Некоторые комментарии:

  • «По умолчанию ввод-вывод ленив» либо крайне вводит в заблуждение, либо совершенно неверен, в зависимости от того, что вы под этим подразумеваете. В m >>= f эффекты m всегда* происходят до того, как f вызывается с аргументом.

  • Типа, возвращаемого действием, недостаточно, чтобы решить, стоит ли принудительное выполнение или нет. Например, можно было бы написать:

      badTriangle :: Int -> Int
      badTriangle n = foldr (+) 0 [0..n]
    
      ioTriangle :: Int -> IO Int
      ioTriangle = pure . badTriangle
    

    Хотя Int является примитивным типом, do !x <- ioTriangle n; ... ведет себя совсем иначе, чем do x <- ioTriangle n; .... Особенно актуальным и довольно распространенным примером здесь является разница между getLine <&> read, который генерирует исключения при использовании его значения (возможно, намного позже в программе), и getLine >>= readIO (он же readLn), который генерирует исключения при вводе неправильного значения.

    В другом направлении getLine сам по себе возвращает непримитивный тип, но принудительная обработка его результата не имеет никакого значения.

  • Знания о том, возвращает ли ваше действие удар или нет, недостаточно, чтобы решить, является ли принуждение хорошей идеей. В некоторых случаях вы можете знать, что значение в конечном итоге будет использовано и что полученное значение использует меньше памяти, чем преобразователь, использованный для его создания; тогда принуждение - хорошая идея. Но любое из этих условий может не сработать. У вас может быть программа, которая никогда не использует полученное значение; тогда немедленное форсирование может потребовать большого количества вычислений, которые никогда не окажутся полезными. Или у вас может быть очень маленькая мысль, описывающая очень большую величину; тогда немедленное форсирование может потребовать больших ассигнований сейчас, которые можно было бы отложить до более подходящего времени.

    Однако допустимо и другое направление рассуждений: если вы знаете, что ваше действие не возвращает сигнал, то вам не нужно форсировать его результат.

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


* Хорошо-хорошо, есть несколько исключений, например readFile и getContents, все из которых где-то в своей реализации используют unsafeInterleaveIO. Но это не нормальная ситуация.

Другие вопросы по теме