One of the unfortunate things in the design of the Monad
typeclass, is that they introduced a function called return
. But although in many imperative programming languages return
is a keyword to return content, in Haskell return
has a totally different meaning, it does not really return something.
You can solve the problem by dropping the return
:
splitStringOnDelimeter :: String -> Char -> [String]
splitStringOnDelimeter "" delimeter = [""]
splitStringOnDelimeter string delimeter =
let split = splitStringOnDelimeter (tail string) delimeter in
if head string == delimeter
then ([""] ++ split)
else ( [( [(head string)] ++ (head split) )] ++ (tail split))
The return :: Monad m => a -> m a
is used to wrap a value (of type a
) in a monad. Since here your signature hints about a list, Haskell will assume that you look for the list monad. So that means that you return
would wrap [""]
into another list, so implicitly with return [""]
you would have written (in this context), [[""]]
, and this of course does not match with [String]
.
The same goes for do
, again you them make a monadic function, but here your function has not much to do with monads.
Note that the name return
is not per se bad, but since nearly all imperative languages attach an (almost) equivalent meaning to it, most people assume that it works the same way in functional languages, but it does not.
Mind that you use functions like head
, tail
, etc. These are usually seen as anti-patterns: you can use pattern matching instead. We can rewrite this to:
splitStringOnDelimeter :: String -> Char -> [String]
splitStringOnDelimeter "" delimeter = [""]
splitStringOnDelimeter (h:t) delimeter | h == delimeter = "" : split
| otherwise = (h : sh) : st
where split@(sh:st) = splitStringOnDelimeter t delimeter
By using pattern matching, we know for sure that the string
has a head h
and a tail t
, and we can directly use these into the expression. This makes the expression shorter as well as more readable. Although if
-then
-else
clauses are not per se anti-patterns, personally I think guards are syntactically more clean. We thus use a where
clause here where we call splitStringOnDelimter t delimeter
, and we pattern match this with split
(as well as with (sh:st)
. We know that this will always match, since both the basecase and the inductive case always produce a list with at least one element. This again allows use to write a neat expression where we can use sh
and st
directly, instead of calling head
and tail
.
If I test this function locally, I got:
Prelude> splitStringOnDelimeter "foo!bar!!qux" '!'
["foo","bar","","qux"]
As take-away message, I think you better avoid using return
, and do
, unless you know what this function and keyword (do
is a keyword) really mean. In the context of functional programming these have a different meaning.