Как абстрагироваться от трансформации "назад и вперед"?
Рассмотрим этот пример (из 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
- который может быть индикатором того, что он немного превышает верхний.
Ответы
Ответ 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 ко всем словам и верните их вместе". Если вы делаете это чаще, подумайте о написании функции.