Как абстрагироваться от трансформации "назад и вперед"? - программирование
Подтвердить что ты не робот

Как абстрагироваться от трансформации "назад и вперед"?

Рассмотрим этот пример (из https://codereview.stackexchange.com/info/23456/crtitique-my-haskell-function-capitalize):

import Data.Char

capWord [] = []
capWord (h:t) = toUpper h : map toLower t

capitalize = unwords . map capWord . words

Есть ли хороший способ абстрагироваться от преобразования "назад и вперед", например. unwords . f . words? Лучшее, что я мог придумать, было

class Lift a b | a -> b where
  up :: a -> b
  down :: b -> a

instance Lift String [String] where
  up = words
  down = unwords

lifted :: (Lift a b) => (b -> b) -> a -> a
lifted f = down . f . up

capitalize = lifted (map capWord)

но он чувствует себя не очень гибким и нуждается в MultiParamTypeClasses, FunctionalDependencies, TypeSynonymInstances и FlexibleInstances - который может быть индикатором того, что он немного превышает верхний.

4b9b3361

Ответ 1

Я бы сказал, что лучший ответ - "нет, потому что абстрагирование от того, что вы ничего не покупаете". На самом деле ваше решение гораздо менее гибкое: в области видимости может быть только один экземпляр Lift String [String], и есть больше способов разделить строку на список строк, чем просто words/unwords (это означает, что вы начнете бросать новые типы или даже более тайные расширения в микс). Держите его просто - оригинальный capitalize просто отлично, как есть.

Или, если вы действительно настаиваете:

lifted :: (a -> b, b -> a) -> (b -> b) -> a -> a
lifted (up, down) f = down . f . up

onWords = lifted (words, unwords)
onLines = lifted (lines, unlines)

capitalize = onWords $ map capWord

Концептуально то же самое, что и ваш класс, за исключением того, что он не злоупотребляет машиной стилей.

Ответ 2

Ваш lifted на самом деле такой же, как dimap из Data.Profunctor:

onWords = dimap words unwords
capitalize = onWords (map capWord)

Это не могло быть направление обобщения, о котором вы думали. Но посмотрите на тип эквивалентной функции в Control.Functor от category-extras:

dimap :: Bifunctor f (Dual k) k k => k b a -> k c d -> k (f a c) (f b d)

Эта версия обобщает ее на все, что является как QFunctor, так и co PFunctor. Не то, что полезно в повседневных сценариях, но интересно.

Ответ 3

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

Например, при использовании words и unwords мы можем сделать объектив worded:

worded :: Simple Iso String [String]
worded = iso words unwords

Затем вы можете использовать его для применения функции внутри объектива, например. lifted f x становится (worded %~ f) x. Единственным недостатком линз является то, что библиотека чрезвычайно сложна и имеет много неясных операторов вроде %~, хотя основная идея объектива на самом деле довольно проста.

EDIT: это не изоморфизм

Я неправильно предположил, что unwords . words эквивалентен функции тождества, а это не так, потому что лишние пробелы между словами теряются, как это правильно указано несколькими людьми.

Вместо этого мы могли бы использовать гораздо более сложный объектив, например следующий, который сохраняет расстояние между словами. Хотя я думаю, что это еще не изоморфизм, это, по крайней мере, означает, что x == (x & worded %~ id), я надеюсь. Это, с другой стороны, ни в коем случае не очень хороший способ делать вещи, а не очень эффективно. Возможно, что индексированная линза самих слов (а не список слов) может быть лучше, хотя она позволяет меньше операций (я думаю, это действительно сложно сказать, когда задействованы линзы).

import Data.Char (isSpace)
import Control.Lens

worded :: Simple Lens String [String]
worded f s =
    let p = makeParts s
    in fmap (joinParts p) (f (takeParts p))

data Parts = End | Space Char Parts | Word String Parts

makeParts :: String -> Parts
makeParts = startPart
    where
      startPart [] = End
      startPart (c:cs) =
          if isSpace c then Space c (startPart cs) else joinPart (Word . (c:)) cs

      joinPart k [] = k [] End
      joinPart k (c:cs) =
          if isSpace c then k [] (Space c (startPart cs)) else joinPart (k . (c:)) cs

takeParts :: Parts -> [String]
takeParts End = []
takeParts (Space _ t) = takeParts t
takeParts (Word s t) = s : takeParts t

joinParts :: Parts -> [String] -> String
joinParts _ [] = []
joinParts (Word _ End) ([email protected](_:_:_)) = unwords ws
joinParts End ws = unwords ws
joinParts (Space c t) ws = c : joinParts t ws
joinParts (Word _ t) (w:ws) = w ++ joinParts t ws

Ответ 4

Как и DarkOtter, Edward Kmett lens вы покрыли, но lens слишком слаб, а Iso немного слишком силен, так как unwords . words не является тождеством. Вместо этого вы можете попробовать Prism.

wordPrism :: Prism' String [String]
wordPrism = prism' unwords $ \s ->
   -- inefficient, but poignant
   if s == (unwords . words) s then Just (words s) else Nothing

Теперь вы можете определить capitalize как

capitalize' :: String -> String
capitalize' = wordPrism %~ map capWord
-- a.k.a    = over wordPrism (map capWord)

но это имеет довольно патологическое поведение по умолчанию для вашего случая. Для String, которые не могут быть отображены как изоморфизмы (строки с несколькими пространствами внутри каждой строки) over wordPrism g == id. Для Prism s должен быть оператор "по возможности", но я не знаю одного. Вы можете определить его, хотя:

overIfPossible :: Prism s t a b -> (a -> b) -> (s -> Maybe t)
overIfPossible p f s = if (isn't p s) then Nothing else Just (over p f s)

capitalize :: String -> Maybe String
capitalize = wordPrism `overIfPossible` map capWord

Теперь, действительно, оба из них довольно неудовлетворительны, поскольку то, что вы действительно хотите, - это использовать все слова и сохранить интервал. Для этого (words, unwords) слишком слаб, вообще говоря, из-за отсутствия изоморфизма, о котором я говорил выше. Вам нужно будет написать свой собственный механизм, который поддерживает пробелы, после чего у вас будет Iso, и вы можете напрямую использовать ответ DarkOtter.

Ответ 5

Это действительно недостаточно гибкое! Как бы вы отделили функцию для работы по очереди? Для этого вам понадобится обертка newtype! Таким образом

newtype LineByLine = LineByLine { unLineByLine :: String }

instance Lift LineByLine [String] where
    up = lines . unLineByLine
    down = LineByLine . unlines

Но теперь нет веских оснований предпочитать пословную версию по очереди.

Я просто использовал бы unwords . map f . words, для меня, что идиоматический "Примените f ко всем словам и верните их вместе". Если вы делаете это чаще, подумайте о написании функции.