Замена символа в строке на строку в Haskell в бесточечном стиле

Идея состоит в том, чтобы написать замену функции, которая принимает три аргумента, подстановочный знак, строку подстановки и входную строку. Пример будет выглядеть как replace '*' "foo" "foo*" = "foobar". Обычно это не было бы слишком большой проблемой, я бы просто написал что-то рекурсивное и проверил, соответствует ли каждый символ в строке моему подстановочному знаку. Однако мне нужно написать это в бесточечном стиле. Я понятия не имею, как это сделать. Я знаю, что могу отбросить самый последний аргумент, входную строку, но после этого я застрял.

Мое решение в стиле без точек: replace wildcard sub = concatMap (\c -> if c==wildcard then sub else [c]) .

Примечание. Нам не разрешено импортировать внешние библиотеки, т.е. Text.Replace.


person Peatherfed    schedule 23.04.2020    source источник
comment
Вам известно о pointfree.io?   -  person Mark Seemann    schedule 23.04.2020
comment
Почему вам нужно писать это в бесточечном стиле? У меня возникли проблемы с представлением любого такого удаленно читаемого определения для replace. (Даже нечитаемый требует, чтобы вы импортировали Data.Bool, чтобы получить доступ к функции bool, необходимой для замены выражения if.)   -  person chepner    schedule 23.04.2020
comment
Я полагаю, что в вашем примере есть ошибка: replace '*' "foo" "foo*" должно производить "foofoo", если я понимаю ваше описание, а не "foobar". Последнее невозможно, если только оно всегда не использует "bar" в качестве значения замены.   -  person Andrew Ray    schedule 23.04.2020


Ответы (3)


Ясно, что писать без точек только ради того, чтобы быть без точек, глупо.

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

concatMap — хороший хук в категорию, потому что это всего лишь >>= в монаде списка. IOW, он переводит стрелку Клейсли A->[B] в функцию преобразования списка в список [A]->[B]. Итак, давайте сосредоточимся на том, как писать

useChar :: Char -> [Char]
useChar = \c -> if c==wildcard then sub else [c]

как стрелка. На самом деле я напишу это как функцию, но вместо этого вы также можете перейти в категорию Клейсли.

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

(&&&) :: Arrow (~>)
   => (b~>x) -> (b~>y) -> (b~>(x,y))

so

import Control.Arrow
sub = "SUBST"
useChar = (==wildcard)&&&(:[])
       >>> \(decision, embd) -> if decision then sub else embd

Обратите внимание, (:[]) — это тождественная стрелка Клейсли для монады списка; Я не собираюсь использовать это, хотя.

Теперь решение if работает с логическими значениями, но они уродливы . Категорически говоря, логическое значение — это просто тип суммы двух типов единиц измерения.

 Bool ≈ Either () () ≡ (()+())
(≡≡) :: Eq a => a -> a -> Either () ()
x ≡≡ y
 | x==y       = Right ()
 | otherwise  = Left ()

Мы могли бы также закодировать некоторую полезную информацию в любую из этих опций (), особенно для опции Right, которая, очевидно, представляет собой просто константу sub.

constRight :: c -> Bool -> Either () c
constRight _ False = Left ()
constRight c True = Right c

useChar = ((==wildcard)>>>constRight sub) &&& (:[])
       >>> \(decision, embd) -> case decision of
              Left () -> embd
              Right theSub -> theSub

или в более общем виде

substRight :: c -> Either a b -> Either a c
substRight _ (Left a) = Left a
substRight c (Right _) = Right c

useChar = ((≡≡wildcard)>>>substRight sub) &&& (:[])
       >>> \(decision, embd) -> case decision of
              Left () -> embd
              Right theSub -> theSub

Очевидно, мы можем заменить слева, а также общий оператор

useChar = ((≡≡wildcard)>>>substRight sub) &&& (:[])
       >>> \(decision, embd) -> substLeft embd decision

Теперь, если мы перевернем кортеж, лямбда адаптируется к каррированной форме substLeft:

useChar = (:[]) &&& ((≡≡wildcard)>>>substRight sub) >>> uncurry substLeft
person leftaroundabout    schedule 24.04.2020

Написанную вами функцию нельзя сократить только с помощью оператора Prelude, потому что невозможно сократить оператор if. (Эрих указывает, что это можно сделать с помощью bool из Data.Bool.) Здесь я разрабатываю альтернативное лечение, которое можно сократить, но я надеюсь, что к концу я убедил вас не делать этого.

Здесь вам может пригодиться функция break. Из Hackage:

примененный к предикату p и списку xs, возвращает кортеж, где первый элемент является самым длинным префиксом (возможно, пустым) xs элементов, которые не удовлетворяют p, а второй элемент является остатком списка

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

splitOnChar :: Char -> String -> (String, String)
splitOnChar char = break (char ==)

Оттуда мы можем разработать функцию, как вы описываете:

replaceChar :: Char -> String -> String -> String
replaceChar char repstr instr =
  case break (char ==) instr of
    (front, _:back) -> front ++ repstr ++ back
    _               -> error "Character to replace not found!"

Это позволяет вам избавиться от оператора if, который невозможно написать без точек. Почему, черт возьми, вы хотите написать эту точку свободной, я не понимаю, но для этого нам нужно пожертвовать нашей обработкой ошибок. Давайте посмотрим на версию, которая отказывается от обработки

replaceChar :: Char -> String -> String -> String
replaceChar char repstr instr =
  let ~(front, _:back) = break (char ==) instr
  in  front ++ repstr ++ back

Затем мы можем заменить front и back выражениями:

replaceChar :: Char -> String -> String -> String
replaceChar char repstr instr =
  let split = break (char ==) instr
  in  (fst split) ++ repstr ++ (tail $ snd split)

Теперь давайте переместим split в конец оператора in:

replaceChar :: Char -> String -> String -> String
replaceChar char repstr instr =
  let split = break (char ==) instr
  in  (\a b -> a ++ repstr ++ b) <$> fst <*> tail . snd $ split

Теперь мы можем заменить split:

replaceChar :: Char -> String -> String -> String
replaceChar char repstr instr =
  (\a b -> a ++ repstr ++ b) <$> fst <*> tail . snd $ break (char ==) instr

Далее давайте восстановим b из нашего лямбда-выражения:

replaceChar :: Char -> String -> String -> String
replaceChar char repstr instr =
  (\a -> ((a ++ repstr) ++)) <$> fst <*> tail . snd $ break (char ==) instr

Затем сделайте то же самое с a:

replaceChar :: Char -> String -> String -> String
replaceChar char repstr instr =
  ((. (repstr ++)) . (++)) <$> fst <*> tail . snd $ break (char ==) instr

Затем замените instr:

replaceChar :: Char -> String -> String -> String
replaceChar char repstr =
  (((. (repstr ++)) . (++)) <$> fst <*> tail . snd) . break (char ==)

Далее проще уменьшить char, поэтому мы просто поменяем местами аргументы и не забудем вернуть их flip позже.

replaceChar :: String -> Char -> String -> String
replaceChar repstr char =
  (((. (repstr ++)) . (++)) <$> fst <*> tail . snd) . break (char ==)

А теперь собственно уменьшим char:

replaceChar :: String -> Char -> String -> String
replaceChar repstr =
  ((((. (repstr ++)) . (++)) <$> fst <*> tail . snd) .) . break . (==)

Теперь нам нужно переставить всю функцию, чтобы получить repstr в конце. Начните с превращения . break . (==) в раздел:

replaceChar :: String -> Char -> String -> String
replaceChar repstr =
  (. break . (==)) $ ((((. (repstr ++)) . (++)) <$> fst <*> tail . snd) .)

Разделить вторую половину:

replaceChar :: String -> Char -> String -> String
replaceChar repstr =
  (. break . (==)) $ (.) $ ((. (repstr ++)) . (++)) <$> fst <*> tail . snd

Раздел <*> tail . snd

replaceChar :: String -> Char -> String -> String
replaceChar repstr =
  (. break . (==)) $ (.) $ (<*> tail . snd) $ ((. (repstr ++)) . (++)) <$> fst

Раздел <$> fst:

replaceChar :: String -> Char -> String -> String
replaceChar repstr =
  (. break . (==)) $ (.) $ (<*> tail . snd) $ (<$> fst) $ (. (repstr ++)) . (++)

Раздел . (++):

replaceChar :: String -> Char -> String -> String
replaceChar repstr =
  (. break . (==)) $ (.) $ (<*> tail . snd) $ (<$> fst) $ (. (++)) $ (. (repstr ++))

Раздел (. (repstr ++)):

replaceChar :: String -> Char -> String -> String
replaceChar repstr =
  (. break . (==)) $ (.) $ (<*> tail . snd) $ (<$> fst) $ (. (++)) $ flip (.) $ (repstr ++)

Раздел (repstr ++):

replaceChar :: String -> Char -> String -> String
replaceChar repstr =
  (. break . (==)) $ (.) $ (<*> tail . snd) $ (<$> fst) $ (. (++)) $ flip (.) $ (++) repstr

Эта-уменьшить:

replaceChar :: String -> Char -> String -> String
replaceChar =
  (. break . (==)) . (.) . (<*> tail . snd) . (<$> fst) . (. (++)) . flip (.) . (++)

И flip, чтобы вернуть аргументы в правильном порядке:

replaceChar :: Char -> String -> String -> String
replaceChar =
  flip $ (. break . (==)) . (.) . (<*> tail . snd) . (<$> fst) . (. (++)) . flip (.) . (++)

Et-voilà: совершенно неразборчивая куча тарабарщины, которая волшебным образом делает то, что вам нужно, по непонятной для человечества причине.

person Andrew Ray    schedule 23.04.2020
comment
Я слепой, или это не работает для входных данных, где есть несколько вхождений подстановочного знака, например. "foo***"? - person Erich; 23.04.2020
comment
Ты прав. OP не определяет желаемое поведение нескольких вхождений. Хотя я, вероятно, мог бы сделать вывод из эталонной реализации. Однако я оставлю это, так как это соответствует реализации ряда replace функций. Я ни за что не повторю это упражнение по эта-редукции из-за незначительных изменений. Именно поэтому что-то такой сложности не должно быть написано без точек. - person Andrew Ray; 23.04.2020
comment
И мне понадобится рекурсия, и fix всегда меня сбивает с толку. - person Andrew Ray; 23.04.2020
comment
Мне очень нравится ваше решение, так как оно показывает, насколько сумасшедшим вы можете быть с Это должно быть бесточечным, чего бы это ни стоило. - person Erich; 23.04.2020

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

Мы можем заменить предложение if на bool из Data.Bool. Прежде всего, мы занимаемся предложением лямбда (\c -> if c==wildcard then sub else [c]). Чтобы освободить нашу функцию replace, я сначала добавил helper, который принимает два дополнительных аргумента, строку замены и предикат, определяющий, какие символы оставить, а какие заменить:

helper :: [b] -> (b -> Bool) -> b -> [b]
helper repl pred c = if pred c then repl else [c]

Используя bool, мы избавляемся от предложения if. Обратите внимание на обратный порядок ветвей if и else, потому что bool работает наоборот.

helper repl pred c = bool [c] repl $ pred c

Поскольку c используется дважды в теле helper, мы можем использовать экземпляр функций Applicative для применения c к нескольким функциям с помощью функций liftAN. Хотя repl не нуждается в c, мы можем превратить его в функцию, которая принимает c, используя const. Таким образом, мы используем liftA3 сейчас:

helper repl pred c = liftA3 bool ((:[])) (const repl) pred $ c

Теперь мы можем легко нарезать c и pred:

helper repl = liftA3 bool ((:[])) (const repl)

и, используя композицию функций для замены (в общем случае) f (g x) на f . g $ x, мы можем переписать это так:

helper repl = liftA3 bool ((:[])) . const $ repl

Где, опять же, мы можем наколоть repl:

helper = liftA3 bool ((:[])) . const

Теперь давайте займемся основной функцией, поместив туда нашего помощника.

replace :: (Foldable t, Eq b) => b -> [b] -> t b -> [b]
replace wildcard sub = concatMap (helper sub (==wildcard))

Так как нам нужны wildcard и sub в другом порядке, мы flip helper:

replace wildcard sub = concatMap ((flip helper) (==wildcard) sub)

Чтобы получить sub из второго члена, мы можем добавить префикс (.):

replace wildcard sub = (.) concatMap ((flip helper) (==wildcard)) $ sub

где мы можем легко нарезать sub:

replace wildcard = (.) concatMap ((flip helper) (==wildcard))

Третий член можно переписать с помощью композиции функций:

replace wildcard = (.) concatMap ((flip helper) . (==) $ wildcard)

Теперь разделите префикс (.):

replace wildcard = (concatMap .) ((flip helper) . (==) $ wildcard)

Наконец, мы можем снова применить шаблон f (g x) = f . g $ x:

replace wildcard = (concatMap .) . ((flip helper) . (==)) $ wildcard

где мы можем нарезать wildcard. Заменив helper нашим определением, мы получим:

replace = (concatMap .) . ((flip $ liftA3 bool ((:[])) . const) . (==))

Хотя это может быть немного более читаемым, чем решение @Andrew Ray, я все же настоятельно рекомендую вам не навязывать безточечный стиль таким образом. Оригинал был намного читабельнее, чем этот бред.

person Erich    schedule 23.04.2020
comment
Каким-то образом я интерпретировал запрет на импорт внешних библиотек как запрет на импорт библиотек, поэтому я не принял во внимание bool, поскольку его нет в Prelude. - person Andrew Ray; 23.04.2020
comment
О, я вижу, если это не разрешено, мой ответ не соответствует требованиям. Хотя я думаю, что показать эту альтернативу все же стоит. - person Erich; 23.04.2020