Skip to content

Instantly share code, notes, and snippets.

@ObjectBoxPC
Last active July 7, 2020 13:48
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ObjectBoxPC/7dbdfedc2482d364a41445e5322e54ff to your computer and use it in GitHub Desktop.
Save ObjectBoxPC/7dbdfedc2482d364a41445e5322e54ff to your computer and use it in GitHub Desktop.
Simple Haskell program to check if the input (command-line arguments) form a pangram, containing all 26 letters of the English alphabet
import Data.Char (toUpper)
import Data.List (intercalate)
import Data.List.NonEmpty (NonEmpty, nonEmpty, toList)
import System.Environment (getArgs)
data PangramResult = Pangram | MissingLetters (NonEmpty Char)
checkPangram :: String -> PangramResult
checkPangram s = case nonEmpty missingLetters of
Just ls -> MissingLetters ls
Nothing -> Pangram
where
missingLetters = filter (not . (`elem` normalized)) ['A'..'Z']
normalized = map toUpper s
getText :: IO String
getText = concat <$> getArgs
showLetters :: NonEmpty Char -> String
showLetters = intercalate ", " . map (:[]) . toList
main = do
text <- getText
let
message = case checkPangram text of
Pangram -> "Pangram"
MissingLetters ls -> "Not a pangram: Missing " ++ showLetters ls
putStrLn message
@friedbrice
Copy link

friedbrice commented Jul 6, 2020

I would not be surprised if I saw any of these.

main = do
  args <- getArgs
  putStrLn (if pangram (concat args) then "Pangram" else "Not a pangram")
main = putStrLn . msg . concat =<< getArgs
  where msg x = if pangram x then "Pangram" else "Not a pangram"
main = do
  arg <- getArgs
  let msg = if pangram (concat args) then "Pangram" else "Not a pangram"
  putStrLn msg

But to answer your question more literally, x >>= return . f is always the same as fmap f x.

@friedbrice
Copy link

friedbrice commented Jul 6, 2020

I could not understand the meaning of allElems as you had it, so I had to write out out. This is what I ended up with.

allElems = flip $ all . flip elem
allElems = (\t ys xs -> t xs ys) (all . (\f u y -> f y u) elem) -- def of `flip`
allElems = (\t ys xs -> t xs ys) (all . (\u y -> elem y u) -- function application
allElems = (\t ys xs -> t xs ys) (\u -> all (\y -> elem y u)) -- function composition
allElems = \ys xs -> (\u -> all (\y -> elem y u)) xs ys -- function application
allElems = \ys xs -> (all (\y -> elem y xs)) ys -- function application
allElems ys xs = all (\y -> elem y xs) ys -- sugar
allElems ys xs = all (\y -> y `elem` xs) ys -- sugar
allElems ys xs = all (`elem` xs) ys -- sugar

In real life, I would expect to see it written as the last line. I doubt very many people would be able to read and readily understand its meaning the way it is on the first line.

@friedbrice
Copy link

friedbrice commented Jul 6, 2020

Idiomatic Haskell requires type signatures on top-level functions. (Type inference is awesome, and it's for lambdas, intermediate expressions, and temporary names, not for top-level functions.) You'd see:

allElems :: Eq a => [a] -> [a] -> Bool
allElems = ...

That would help the reader readily apprehend the meaning. Furthermore, I could try to use the name to convey the positional relation between the arguments, e.g. intead of allElems ys xs, change the name and write it infix as

subset :: Eq a => [a] -> [a] -> Bool
ys `subset` xs = ...

Between the name and the type signature, the meaning of the function and the order of its arguments should be clear. Users shouldn't have to look at the implementation. That way, you can have your cake and eat it too.

subset :: Eq a => [a] -> [a] -> Bool
subset = flip $ all . flip elem

Now it doesn't matter that the implementation is quite erudite, because I no longer need to understand the implementation. I can still use this function because I know both its name and its type signature. I would know to write my code as

pangram = (['A' .. 'Z'] `subset`) . map toUpper

(By the way, the ['A' .. 'Z'] is legal Haskell that produces the string you need.)

@ObjectBoxPC
Copy link
Author

@friedbrice Thanks a lot for your detailed comments. I've made changes based on your suggestions.

Some specific responses:

  • I originally had something very similar to your alternative implementation of allElems/subset, but I suppose I went a bit overboard in trying to make it point-free. I wasn't used to using functions as infix operators so I had (flip elem) instead of (`elem` xs) However, thinking of elem and subset as set symbols does make the infix version more intuitive to me.
  • My implementation of main was meant to convey a sort of pipeline like in a Unix shell. But do-notation version does make it shorter so I went for that.

@friedbrice
Copy link

friedbrice commented Jul 6, 2020

@ObjectBoxPC I'm glad it was helpful. I definitely relate to the tendency to want to write your main as a pipeline, so I tend to favor the putStrLn . msg . concat =<< getArgs style. It never bothered me that the entrance to the pipeline is on the right side, because that's how function application is written in every programming language and traditionally on paper.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment