Можно ли написать эту функцию в бесточечном стиле? Если нет, то почему? - PullRequest
14 голосов
/ 08 июля 2020

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

\[x] -> x

http://pointfree.io/, похоже, не может писать это без точек. Значит ли это, что так писать нельзя? Если да, то какова теоретическая причина этого?

Я могу только заметить, что приведенная выше функция является «урезанной» версией head (или last, fwiw), которая может работать только с одноэлементными списками. . Действительно, применительно к не одноэлементным спискам, он ошибается в ghci:

*** Exception: <interactive>:380:5-13: Non-exhaustive patterns in lambda

Возможно, «неполная полнота» в шаблоне является причиной того, что некоторые функции не могут быть написаны в стиле без точек. ?

Отредактируйте в свете ответов:

Я не ожидал, что ответы на мой вопрос могут быть такими сложными (мне кажется, я просто подумал, что короткий ответ был нет, на самом деле не может ), поэтому мне нужно найти время, чтобы внимательно их прочитать, немного поэкспериментировать и осмыслить их, иначе я не могу решить, какой из них следует принять. На данный момент +1 к ответу Джона Парди, который я мог легко понять до . Здесь я бы остановился в обычном коде .

Ответы [ 3 ]

19 голосов
/ 08 июля 2020

Ну, тип данных - это не функция. Пока ваша функция не разворачивает какие-либо значения данных (т.е. она просто перетасовывает их между функциями / конструкторами), вы можете писать ее без точек, но просто нет синтаксиса для сопоставления без точек. Однако для каждого типа данных вам понадобится только одна функция без точек: свертка. В Haskell типы данных в значительной степени определяются их свертками. Принимая свертки соответствующих типов данных за примитивы, вы можете переписать любую функцию без точки. Обратите внимание, что на самом деле существует несколько возможных «складок». Для [a] рекурсивный (который происходит из кодировки Чёрча / Бёма-Берардуччи) равен foldr :: (a -> b -> b) -> b -> [a] -> b. Другой возможный сверток - это «case -but-it-a-function», (a -> [a] -> b) -> b -> [a] -> b, который исходит из кодировки Скотта (рекурсия затем может быть восстановлена ​​с помощью fix, который является еще одним «точечным примитивом без точек») , но, как отмечает @SilvioMayolo, в стандартной библиотеке такой функции нет. Любой из них подойдет, но у нас нет предопределенного последнего, поэтому давайте просто используем foldr.

\[x] -> x

можно записать

fst . foldr (\x f -> (snd f x, \_ -> error "got (_ : _ : _) wanted [x]")) (error "got [] wanted [x]", id)
-- I don't care enough to replicate the exact exceptions.
-- this is "flattened" from
let fold [] = (error "got [] wanted [x]", id)
    fold (x : xs) = (snd (fold xs) x, \_ -> error "got (_ : _ : _) wanted [x]")
in  fst . fold

fold возвращает пару, в основном (what to return if this was the entire list, how to transform the head if it wasn't). Для [] мы хотим вернуть ошибку, если это был весь список, но в противном случае пройти через элемент прямо перед тем, как мы нажмем []. Для x : xs, если есть предшествующий ему элемент, мы хотим проигнорировать его и вернуть ошибку, а если нет, мы хотим передать его в snd (fold xs), который проверяет, дает ли xs = [] или иначе ошибка. Мы удалили все совпадения, поэтому просто пропустите это через pointfree.io, чтобы получить \x f -> _ в аргументе foldr out:

behead = fst . foldr (flip flip (const (error "got (_ : _ : _) wanted [x]")) . ((,) .) . flip snd) (error "got [] wanted [x]", id)
ghci> :t behead
behead :: Foldable t => t c -> c
ghci> behead []
*** Exception: got [] wanted [x]
ghci> behead [1]
1
ghci> behead [1, 2]
*** Exception: got (_ : _ : _) wanted [x]
ghci> behead [1..]
*** Exception: got (_ : _ : _) wanted [x]

Lovely.

Примечание: a в предыдущей версии этого ответа использовался «встроенный» вспомогательный тип данных, в основном потому, что он просто «пришел ко мне», когда я его писал. Однако он не смог правильно обработать бесконечные списки (behead [1..] зависнет). Эта версия использует встроенные пары в качестве вспомогательного типа данных, которые имеют достаточную библиотечную поддержку, поэтому мне не нужно встраивать их, чтобы сделать их бессмысленными. Немного сложнее встроить (,), тем самым устраняя смысловую нагрузку внутри реализаций fst и snd, но это все еще возможно, используя этот новый тип:

newtype Pair a b = Pair { unPair :: forall r. (a -> b -> r) -> r }

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

-- residual pointfullness can be reduced by pointfree.io
\xs -> foldr (\x r f -> f (r (const id) x) (\_ -> error "got (_ : _ : _) wanted [x]")) (\f -> f (error "got [] wanted [x]") id) xs (\x _ _ -> x) undefined
9 голосов
/ 08 июля 2020

Конечно, почти все можно сделать бессмысленным. Сложность в том, какие функции вы разрешите в результирующем выражении. Если мы выполняем сопоставление с образцом, нам обычно нужна функция сворачивания для сопоставления. Так, например, если мы сопоставили шаблон с Maybe a, нам нужно будет заменить его на maybe. Точно так же шаблоны Either a b могут быть записаны в виде either.

Обратите внимание, что шаблон в подписях

data Maybe a = Nothing | Just a

maybe :: b -> (a -> b) -> (Maybe a -> b)

Maybe a имеет два конструктора, один не принимает аргументов, а другой принимает a. Итак, maybe принимает два аргумента: один является нулевой функцией (b), а другой принимает a (a -> b), а затем возвращает функцию из Maybe a -> b. Такой же шаблон присутствует в either

data Either a b = Left a | Right b

either :: (a -> c) -> (b -> c) -> (Either a b -> c)

двух случаях. Первый принимает a и производит все, что c мы хотим. Второй принимает b и производит все, что c мы хотим. В каждом случае нам нужна одна функция для каждого возможного члена в типе суммы.

Чтобы систематически освобождать от точек функцию вроде \[x] -> x, нам понадобится аналогичная свертка. [a] объявлен как, по сути,

data [a] = [] | a : [a]

Поэтому нам понадобится функция с этой сигнатурой

list :: b -> (a -> [a] -> b) -> ([a] -> b)

Теперь flip foldr приближается

flip foldr :: b -> (a -> b -> b) -> ([a] -> b)

Но это рекурсивно. Он вызывает предоставленную ему функцию в [a] части a : [a]. Мы хотим истинного сворачивания, которого нет в базовых библиотеках Haskell. Быстрый поиск в Google говорит нам, что эта функция существует в пакете под названием extra. Конечно, для этого небольшого примера мы можем просто очень легко написать его сами.

list :: b -> (a -> [a] -> b) -> ([a] -> b)
list f g x = case x of
               [] -> f
               (y:ys) -> g y ys

Теперь мы можем легко применить его к вашему \[x] -> x. Во-первых, давайте напишем, что на самом деле делает ваша функция, включая все беспорядочные undefined случаи (здесь я буду использовать undefined вместо длинного сообщения об ошибке, для краткости)

func :: [a] -> a
func x = case x of
           [] -> undefined
           (y:ys) -> case ys of
                       [] -> y
                       (_:_) -> undefined

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

func :: [a] -> a
func x = case x of
         [] -> undefined
         (y:ys) -> list y undefined ys

А теперь трансформируем и внешний корпус

func :: [a] -> a
func x = list undefined (\y -> list y undefined) x

Итак, у нас есть

func :: [a] -> a
func = list undefined (\y -> list y undefined)

Или, если мы хотим быть по-настоящему без ума от этого

func :: [a] -> a
func = list undefined (flip list undefined)

Но этой функции нет в базе

Да, это правда. Мы как бы обманули, используя фолд, которого не было. Если мы хотим делать это систематически, нам нужен этот оператор сворачивания. Но без него мы все равно можем объединить его вместе с foldr1, что достаточно для наших конкретных целей.

func' :: [a] -> a
func' = foldr1 (const (const undefined))

Итак, чтобы ответить на ваш вопрос, мы не всегда можем систематически заменять сопоставление с образцом, как в вашем пример с pointfree, , если у нас нет функции сворачивания с правильной подписью. К счастью, эту функцию всегда можно написать для любого типа данных Haskell 98 (возможно, также и для GADT, но я не рассматривал эту возможность сколько-нибудь подробно). Но даже без этой поддержки мы все равно можем заставить его работать, вроде как.

6 голосов
/ 09 июля 2020

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

  • Пусто : У нас нет еще не видел элемента; сохранить

  • Полный : Мы видели элемент; вызвать ошибку

Если конечное состояние - Empty , мы также выдаем ошибку. Этот аккумулятор можно естественным образом представить как Maybe:

fromSingleton :: (Foldable t) => t a -> a
fromSingleton
  = fromMaybe (error "empty list")
  . foldr (flip maybe (error "plural list") . Just) Nothing

На этом я остановлюсь в обычном коде. Но ...

Если вы не хотите использовать вспомогательный тип данных, вы можете избавиться от Maybe, представив его в кодировке Бема – Берардуччи:

type Maybe' r a
  = r          -- ‘Nothing’ continuation
  -> (a -> r)  -- ‘Just’ continuation
  -> r         -- Result

just' :: a -> Maybe' r a
-- just' = \ x _n j -> j x
just'
  = const     -- Ignore ‘Nothing’ continuation
  . flip ($)  -- Apply ‘Just’ continuation to value

nothing' :: Maybe' r a
-- nothing' = \ n _j -> n
nothing' = const  -- Ignore ‘Just’ continuation

maybe' :: r -> (a -> r) -> Maybe' r a -> r
-- maybe' = \ n j k -> k n j
maybe'
  = flip      -- Apply to ‘Just’ continuation
  . flip ($)  -- Apply to ‘Nothing’ continuation

fromMaybe' :: r -> Maybe' r r -> r
-- fromMaybe' = \ n k -> k n id
fromMaybe' = flip maybe' id  -- Pass ‘id’ as ‘Just’ continuation

Однако, мы не можем просто произвести полную замену Just на just', maybe на maybe' и так далее; типы не сработают:

> :t fromMaybe' (error "empty list") . foldr (flip maybe' (error "plural list") . just') nothing'

<interactive>:…:…: error:
    • Occurs check: cannot construct the infinite type: c ~ Maybe' c c
      Expected type: c -> Maybe' c c -> Maybe' c c
        Actual type: c -> Maybe' (Maybe' c c) c -> Maybe' c c
    • In the first argument of ‘foldr’, namely
        ‘(flip maybe' (error "plural list") . just')’
      In the second argument of ‘(.)’, namely
        ‘foldr (flip maybe' (error "plural list") . just') nothing'’
      In the expression:
        fromMaybe' (error "empty list")
          . foldr (flip maybe' (error "plural list") . just') nothing'

Проблема в том, что мы возвращаем Maybe' из продолжения Maybe', а компилятор пытается унифицировать два типа результатов. Одним из решений является сначала eta-expand, чтобы дать контролеру типов знать, где мы хотим создать отдельную функцию:

> :t fromMaybe' (error "empty list") . foldr (\ x acc -> \ n j -> maybe' (just' x n j) (error "plural list") acc) nothing'

fromMaybe' (error "empty list") . foldr (\ x acc -> \ n j -> maybe' (just' x n j) (error "plural list") acc) nothing'
  :: Foldable t => t c -> c

Затем мы можем постепенно переписать в безточечную форму:

fromSingleton
  = fromMaybe' (error "empty list")
  . foldr
    (\ x acc
      -> \ n j
        -> maybe'
          (just' x n j)
          (error "plural list")
          acc)
    nothing'

-- Move ‘n’ & ‘j’ past ‘error …’ with ‘flip’:

fromSingleton
  = fromMaybe' (error "empty list")
  . foldr
    (\ x acc
      -> \ n j
        -> flip maybe'
           ----
          (error "plural list")
          (just' x n j)
          acc)
    nothing'

-- Move ‘n’ & ‘j’ past ‘acc’ with ‘flip’ again:

fromSingleton
  = fromMaybe' (error "empty list")
  . foldr
    (\ x acc
      -> \ n j
        -> flip (flip maybe' (error "plural list")) acc
           ----
          (just' x n j))
    nothing'

-- Eta-reduce ‘j’ with composition:

fromSingleton
  = fromMaybe' (error "empty list")
  . foldr
    (\ x acc
      -> \ n
        -> flip (flip maybe' (error "plural list")) acc
          . just' x n)
          --
    nothing'

-- Eta-reduce ‘n’ with ‘fmap’ (to map “under” an argument):

fromSingleton
  = fromMaybe' (error "empty list")
  . foldr
    (\ x acc
      -> fmap (flip (flip maybe' (error "plural list")) acc)
         ----
        . just' x)
    nothing'

-- Move ‘x’ rightward with ‘flip’ on the outside:

fromSingleton
  = fromMaybe' (error "empty list")
  . foldr
    (flip (\ acc x
     ----
      -> fmap (flip (flip maybe' (error "plural list")) acc)
        . just' x))
    nothing'

-- Replace composition with ‘fmap’:

fromSingleton
  = fromMaybe' (error "empty list")
  . foldr
    (flip (\ acc x
      -> fmap (fmap (flip (flip maybe' (error "plural list")) acc))
         ----
        (just' x)))
    nothing'

-- Eta-reduce ‘x’ with composition:

fromSingleton
  = fromMaybe' (error "empty list")
  . foldr
    (flip (\ acc
      -> fmap (fmap (flip (flip maybe' (error "plural list")) acc))
        . just'))
        --
    nothing'

-- Replace composition with ‘fmap’:

fromSingleton
  = fromMaybe' (error "empty list")
  . foldr
    (flip (\ acc
      -> fmap (fmap (fmap (flip (flip maybe' (error "plural list")) acc)))
         ----
        just'))
    nothing'

-- Move ‘acc’ rightward with ‘flip’:

fromSingleton
  = fromMaybe' (error "empty list")
  . foldr
    (flip (\ acc
      -> flip fmap just'
         ----
        (fmap (fmap (flip (flip maybe' (error "plural list")) acc)))))
    nothing'

-- Eta-reduce with composition:

fromSingleton
  = fromMaybe' (error "empty list")
  . foldr
    (flip
      (flip fmap just'
        . fmap . fmap . flip (flip maybe' (error "plural list"))))
        --     -      -
    nothing'

Это также полностью бессмысленен (гораздо менее читабелен, чем наш исходный код, но лучше, чем генерирует pointfree). Фактически, хорошая практика в бесточечном коде - использовать множество небольших вспомогательных определений, таких как fromMaybe', вместо встраивания всего, но мы можем перейти к встраиванию их определений.

Однако вы не можете вставьте их наивно и получите точно такой же тип - если вы это сделаете, вы получите (Foldable t) => t (a -> b) -> a -> b. Это может быть хорошим упражнением для работы там, где вам нужно расширить eta и перезаписать, чтобы получить ожидаемый тип, (Foldable t) => t a -> a.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...