2
\$\begingroup\$

Related to this code golf challenge, I tried to find acronyms with Haskell without using regular expressions.

The idea is to split the input string at every space or dash before finally gluing the heads of these parts together, if they are uppercase.

This is my code:

import System.Environment
import Data.Char

main :: IO ()
main = do
    [inp] <- getArgs -- get input from the command line
    putStrLn $ getAcronym inp

getAcronym :: String -> String
getAcronym [] = []
getAcronym s  = foldr step [] parts
    where parts = split isWordSep s -- split into words
          step x acc = if isUpper . head $ x
                       then head x : acc else acc -- glue uppercase heads together

split :: (a -> Bool) -> [a] -> [[a]]
split p []       = []
split p s@(x:xs)
  | p x       = split p xs -- discard trailing white spaces
  | otherwise = w : split p r -- continue with the rest
      where (w, r) = break p s -- seperate prefix

isWordSep :: Char -> Bool
isWordSep x = x == ' ' || x == '-'

As this really seems like a very simple problem, my code looks like way too much complexity.

Do you have any helpful improvements to slim down my code?

\$\endgroup\$
4
  • 3
    \$\begingroup\$ getAcronym = filter isUpper . map head . wordsBy isWordSep, using Data.List.Split's wordsBy. \$\endgroup\$
    – Gurkenglas
    Jul 23, 2017 at 23:26
  • \$\begingroup\$ That's a nice idea. I didn't know wordsBy. \$\endgroup\$
    – Erich
    Jul 24, 2017 at 5:01
  • \$\begingroup\$ @Erich keep in mind that wordsBy is from the package split. I'm not sure whether that is allowed on PCG. \$\endgroup\$
    – Zeta
    Jul 24, 2017 at 12:22
  • \$\begingroup\$ @Zeta Me neither, but it doesn't really matter, as it was just for practicing purposes. On PCG I would use Regular Expressions anyway. \$\endgroup\$
    – Erich
    Jul 24, 2017 at 12:29

2 Answers 2

2
\$\begingroup\$

Since you asked for help slimming down your code, I'll give you some hints that might otherwise be inadvisable.

  1. Don't use do-notation. In fact, you can replace

    main :: IO ()
    main = do
        [inp] <- getArgs -- get input from the command line
        putStrLn $ getAcronym inp
    

    with

    main :: IO ()
    main = putStrLn =<< (fmap (getAcronym . head) getArgs)
    
  2. I'd replace isWordSep to take advantage of pattern matching. As an example:

    isWordSep :: Char -> Bool
    isWordSep ' ' = True
    isWordSep '-' = True
    isWordSep _   = False
    
  3. Consider using a parser library. Monadic parser combinators are quite nice, and they accomplish what you'd probably use regular expression for, but with far nicer error messages and type safety. Here's one example using megaparsec:

    import           Control.Monad
    import           Data.Void
    import           Text.Megaparsec
    import           Text.Megaparsec.Char
    
    type Parser = Parsec Void String
    
    uppercaseWord :: Parser Char
    uppercaseWord = do
        h <- upperChar
        many $ noneOf "- "
        pure h
    
    acronymP :: Parser String
    acronymP = some $ do
        w <- uppercaseWord
        void (oneOf " -") <|> eof
        pure w
    
    acronym :: String -> IO ()
    acronym = parseTest acronymP
    

    You can unpack the do-notation to get:

    acronym :: String -> IO ()
    acronym = parseTest (acronymP :: Parser String)
        where
            acronymP = some (uppercaseWord >>= ((void (oneOf " -") <|> eof) >>) . pure)
            uppercaseWord = upperChar >>= ((many (noneOf "- ") >>) . pure)
    

    which, surprisingly enough, does indeed work.

\$\endgroup\$
1
\$\begingroup\$

With the help of Gurkenglas, I have found a good solution for this problem:

First, the getAcronym function can be dramatically reduced by using higher order functions and function composition:

getAcronym :: String -> String
getAcronym = filter isUpper . map head . split isWordSep

Second, the split function can be replaced with Data.List.Split's wordsBy function, reducing the whole code to the following:

import System.Environment
import Data.Char
import Data.List.Split (wordsBy)

main :: IO ()
main = do
    [inp] <- getArgs -- get input from the command line
    putStrLn $ getAcronym inp

getAcronym :: String -> String
getAcronym = filter isUpper . map head . wordsBy isWordSep

isWordSep :: Char -> Bool
isWordSep x = x == ' ' || x == '-'
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.