I'm trying to write a Haskell function that takes a string of pairs of letters, and exchanges the letters of the pair in a string of all letters, but what I've come up with feels awkward and unidiomatic.
I have
swap a b = map (\x-> if x == a then b else if x == b then a else x)
sub n = foldr (.) id (zipWith swap (head <$> splitOn "." n) (last <$> splitOn "." n)) ['A'..'Z']
which works well enough giving
> sub "RB.XD.EU.ZM.IJ"
"ARCXUFGHJIKLZNOPQBSTEVWDYM"
and
> sub "YC.LU.EB.TZ.RB.XD.IJ"
"ARYXBFGHJIKUMNOPQESZLVWDCT"
but I'm new to Haskell and feel like my approach — especially my swap
helper function (which I only use here) — is more elaborate than it needs to be.
Is there a better, more idiomatic, approach to this problem; especially one that takes advantage of a language feature, builtin, or library function that I've missed?
Doing a left fold over the substitution list makes the code shorter:
import Data.List
import Data.List.Split
sub = foldl' swap ['A'..'Z'] . splitOn "." . reverse
where
swap az [a,b] = map (\x -> if x == a then b else if x == b then a else x) az
Drop the reverse if you don't care whether EB
or RB
is swapped first.
If you'd want to replace instead of a swap:
import Data.List
import Data.List.Split
replace needle replacement haystack =
intercalate replacement (splitOn needle haystack)
rep = foldl' replace' ['A'..'Z'] . splitOn "."
where
replace' az [a,b] = replace [a] [b] az
I'd break the problem down a bit more. It's important to remember that shorter code is not necessarily the best code. Your implementation works, but it's too compact for me to quickly understand. I'd recommend something more like
import Data.Maybe (mapMaybe)
swap = undefined -- Your current implementation is fine,
-- although you could rewrite it using
-- a local function instead of a lambda
-- |Parses the swap specification string into a list of
-- of characters to swap as tuples
parseSwap :: String -> [(Char, Char)]
parseSwap = mapMaybe toTuple . splitOn "."
where
toTuple (first:second:_) = Just (first, second)
toTuple _ = Nothing
-- |Takes a list of characters to swap and applies it
-- to a target string
sub :: [(Char, Char)] -> String -> String
sub charsToSwap = foldr (.) id (map (uncurry swap) charsToSwap)
The equivalent to your sub
function would be
sub swapSpec = foldr (.) id (map (uncurry swap) $ parseSwap swapSpec)
But the former is probably easier to understand for most haskellers. You could also do more transformations more easily to your swap specification as a list of tuples making it more powerful overall. You essentially decouple the representation of the swap specification and the actual swapping. Even for small programs like this it's important to maintain loose coupling so that you develop a habit for when you write larger programs!
This implementation also avoids recalculating splitOn
for the swap specification string.
(I wasn't able to execute this code because I'm on a computer without Haskell installed, if anyone notices any bugs please edit to fix.) Tried it out in FPComplete, output matches @raxacoricofallapatorius'.
Some things I noticed from reading your code (I haven't tried to rewrite it). My first suggestion involves separation of concerns:
I'm trying to write a Haskell function that takes a string of pairs of letters, and exchanges the letters of the pair in a string of all letters
That means the a more natural type for your function would be:
sub :: [(Char, Char)] -> String -> String
Or, using Data.Map
for more efficient lookups:
sub :: Map Char Char -> String -> String
Which is a lot more precise than taking a string with dot-separated pairs. You can then generate the associations between Char
s in a separate step:
parseCharPairs :: String -> Map Char Char
Ideally you should also handle invalid inputs (e.g. AB.CDE
) and empty input strings.
my swap
helper function (which I only use here)
Then you probably should define it in a where
clause. I would also avoid the name swap
, as there is a relatively common function in Data.Tuple
with the same name (swapLetters
might be a nice choice).
sub n = foldr (.) id -- etc.
foldr (.) id (fmap f xs) y
is the same thing as foldr f y xs
. I'm almost certain this can be rewritten in a simpler way.