I currently have the following code in Haskell
splitStringOnDelimeter :: String -> Char -> [String]
splitStringOnDelimeter "" delimeter = return [""]
splitStringOnDelimeter string delimeter = do
let split = splitStringOnDelimeter (tail string) delimeter
if head string == delimeter
then return ([""] ++ split)
else return ( [( [(head string)] ++ (head split) )] ++ (tail split))
If I run it in a Haskell terminal (i.e. https://www.tryhaskell.org) with values for the return statement such as ( [( [(head "ZZZZ")] ++ (head ["first", "second", "third"]) )] ++ (tail ["first", "second", "third"]))
or [""] ++ ["first", "second", "third"]
or [""]
then I receive the correct types from the terminal which is different to my local stack compiler. Furthermore, if I also change the top return statement to return ""
then it doesn't complain about that statement which I'm pretty sure is incorrect.
My local compiler works fine with the rest of my Haskell codebase which is why I think it might be something wrong with my code...
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With