Композиция функций Do Notation

Есть ли синтаксический сахар "do notation" для простой композиции функций?

(i.e. (.) :: (b -> c) -> (a -> b) -> a -> c)

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

Я бы предпочел не использовать расширение RebindableSyntax, если это возможно.

Я ищу что-то вроде этого:

composed :: [String] -> [String]
composed = do
    fmap (++ "!!!")
    maxLength <- maximum . fmap length
    filter ((== maxLength) . length)

composed ["alice", "bob", "david"]
-- outputs: ["alice!!!", "david!!!"]

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

Возможно, я мог бы сделать что-то подобное с монадой состояния?

Спасибо за вашу помощь!

Изменить

Такая штука вроде работает:

split :: (a -> b) -> (b -> a -> c) -> a -> c
split ab bac a = bac (ab a) a

composed :: [String] -> [String]
composed = do
    fmap (++ "!!!")
    split 
        (maximum . fmap length)
        (\maxLength -> (filter ((== maxLength) . length)))

person Chris Penner    schedule 10.11.2016    source источник
comment
Пожалуйста, укажите, какую композицию вы на самом деле пытаетесь получить здесь. По крайней мере, дайте подпись типа, которую вы хотите для composed!   -  person leftaroundabout    schedule 10.11.2016
comment
Просто обычная композиция функций, но с возможностью сохранения промежуточных результатов для использования в последующих вызовах функций (см. пример). Я добавил тип.   -  person Chris Penner    schedule 10.11.2016
comment
Есть ли причина, по которой вам нужно написать это в бесточечном стиле? Это должно быть легко в неточечном стиле.   -  person user253751    schedule 10.11.2016
comment
@immibis Думаю, я мог бы немного разделить некоторые функции. Однако этот пример упрощен, в моем реальном случае использования я повторно использую одно из более ранних значений, таких как 6 или 7 вызовов функций позже, и я не уверен, как бы я сделал это чистым. У вас есть пример того, как это может помочь?   -  person Chris Penner    schedule 10.11.2016
comment
Что-то вроде composed xs = filter ((== maxLength) . length) modified where modified = fmap (++ "!!!") xs maxLength = maximum $ fmap length modified (с соответствующими пробелами)   -  person user253751    schedule 11.11.2016


Ответы (4)


Как уже упоминалось leftaroundabout, вы можете использовать Arrows для написания своей функции. Но в компиляторе ghc Haskell есть функция, которая представляет собой proc-нотацию для стрелок. Она очень похожа на известную do-нотацию, но, к сожалению, мало кто о ней знает.

С помощью proc-нотации вы можете написать нужную функцию следующим более удобным и элегантным способом:

{-# LANGUAGE Arrows #-}

import Control.Arrow (returnA)
import Data.List     (maximum)

composed :: [String] -> [String]
composed = proc l -> do
    bangedL <- fmap (++"!!!")        -< l
    maxLen  <- maximum . fmap length -< bangedL
    returnA -< filter ((== maxLen) . length) bangedL

И это работает в ghci как и ожидалось:

ghci> composed ["alice", "bob", "david"]
["alice!!!","david!!!"]

Если вам интересно, вы можете прочитать несколько руководств с красивыми картинками, чтобы понять, что такое стрелка и как работает эта мощная функция, чтобы вы могли глубже погрузиться в нее:

https://www.haskell.org/arrows/index.html

https://en.wikibooks.org/wiki/Haskell/Understanding_arrows

person Shersh    schedule 10.11.2016
comment
Приятно отметить, хотя я бы сказал, что сила нотации proc заключается в том, что она позволяет вам писать довольно общие композиции стрелок, как если бы они были функциями в синтаксисе лямбда. Если вы работаете с категорией (->), то нет особых причин не использовать привязку лямбда-выражений / let...in для выполнения того же самого, если вы все равно собираетесь давать имена этим промежуточным результатам. - person leftaroundabout; 10.11.2016
comment
@leftaroundabout Да, кроме (->), существуют стрелки, и вы можете написать композиции функций общей версии, хотя я не делал этого для этого случая, чтобы упростить пример. Ваша версия следует мыслям из этого сообщения в блоге: degoes.net/articles/insufficiently-polymorphic. иногда наличие имен переменных может облегчить понимание кода (особенно для стрелок). - person Shersh; 10.11.2016

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

import Control.Arrow

composed = fmap (++ "!!!")
       >>> ((. length) . (==) . maximum . fmap length &&& id)
       >>> uncurry filter

Это определенно не является хорошим понятным для человека кодом.

Монада состояния, казалось бы, тоже допускает что-то связанное, но проблема в том, что тип состояния фиксируется через монадическую цепочку блока do. Это недостаточно гибко, чтобы подбирать значения разных типов по всей цепочке композиции. Хотя это, безусловно, можно обойти (среди них, действительно, RebindableSyntax), это тоже не очень хорошая идея, IMO.

person leftaroundabout    schedule 10.11.2016
comment
Хм, Интересно! Я попробовал что-то подобное в приведенном выше редактировании, мне все равно хотелось бы увидеть что-то более элегантное, хотя все это немного неловко :/ - person Chris Penner; 10.11.2016

Тип (<*>), специализированный для экземпляра функции Applicative:

(<*>) :: (r -> a -> b) -> (r -> a) -> (r -> b)

Результирующая функция r -> b передает свой аргумент обеим функциям r -> a -> b и r -> a, а затем использует значение a, полученное функцией r -> a, в качестве второго аргумента функции r -> a -> b.

Какое это имеет отношение к вашей функции? filter — это функция двух аргументов, предиката и списка. Теперь ключевым аспектом того, что вы пытаетесь сделать, является то, что предикат генерируется из списка. Это означает, что ядро ​​вашей функции может быть выражено в терминах (<*>):

-- Using the predicate-generating function from leftaroundabout's answer.
maxLengthOnly :: Foldable t => [t a] -> [t a]
maxLengthOnly = flip filter <*> ((. length) . (==) . maximum . fmap length)

composed :: [String] -> [String]
composed = maxLengthOnly . fmap (++ "!!!")

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

Поскольку экземпляр функций Applicative эквивалентен по мощности экземпляру Monad, maxLengthOnly можно также сформулировать так:

maxLengthOnly = (. length) . (==) . maximum . fmap length >>= filter

(Кстати, split, которое вы добавили к своему вопросу, — это (>>=) для функций.)

Другой способ записи с Applicative:

maxLengthOnly = filter <$> ((. length) . (==) . maximum . fmap length) <*> id

Не случайно это очень похоже на решение leftaroundabout: для функций (,) <$> f <*> g = liftA2 (,) f g = f &&& g.

Наконец, также стоит отметить, что хотя и заманчиво заменить id в последней версии maxLengthOnly на fmap (++ "!!!"), это не сработает, поскольку fmap (++ "!!!") изменяет длину строк и, следовательно, влияет на результат предиката. Однако с функцией, которая не делает недействительным предикат, это будет работать очень хорошо:

nicerComposed = filter
    <$> ((. length) . (==) . maximum . fmap length) <*> fmap reverse
GHCi> nicerComposed ["alice","bob","david"]
["ecila","divad"]
person duplode    schedule 10.11.2016

То, что у вас есть, по сути является фильтром, но функция фильтрации меняется по мере того, как вы перебираете список. Я бы смоделировал это не как "разветвленную" композицию, а как складку, используя следующую функцию f :: String -> (Int, [String]):

  1. Возвращаемое значение поддерживает текущий максимум и все строки этой длины.
  2. Если первый аргумент короче текущего максимума, отбросить его.
  3. Если первый аргумент совпадает с текущим максимумом, добавьте его в список.
  4. Если первый аргумент длиннее, сделайте его длину новой максимальной и замените текущий список вывода новым списком.

После завершения сворачивания вы просто извлекаете список из кортежа.

-- Not really a suitable name anymore, but...
composed :: [String] -> [String]
composed = snd . foldr f (0, [])
    where f curr (maxLen, result) = let currLen = length curr
                                    in case compare currLen maxLen of
                                       LT -> (maxLen, result)       -- drop
                                       EQ -> (maxLen, curr:result)  -- keep
                                       GT -> (length curr, [curr])  -- reset
person chepner    schedule 10.11.2016