Reputation: 9360
I am receiving 2 string path's as an argument: input
and output
and i want to read the file from input
path and write it to the output
path.
I want to treat all 4 scenarios regarding input / output paths.When one of them is null i want to give it a default value.Is there anything like a coalesce operator?I do not want to rewrite the do clause for all scenarios:
Scenarios
func null _ -> {do clause}
_ null -> {do clause}
_ _ -> {do clause}
x y -> {do clause}
let defaultInPath="inPath.txt"
defaultOutPath="outPath.txt"
What i want to achieve -do clause:
do
text<-readFile input??defaultIn
writeFile output??defaultOut text
return text
P.S I am new to Haskell and i am really trying to get a grasp of it.
Upvotes: 1
Views: 728
Reputation: 233237
The other answers already here are more practical than what follows, but if you're interested in a more conceptual view of things, then read on.
First, Haskell doesn't have null references, but if you want to model missing values, you can use Maybe
. If, for example, you want to treat the empty string as a missing value, you can write a conversion function like this:
maybeFromNull :: Foldable t => t a -> Maybe (t a)
maybeFromNull xs = if null xs then Nothing else Just xs
You use it like this:
*Q49616294> maybeFromNull "foo"
Just "foo"
*Q49616294> maybeFromNull ""
Nothing
The reason that this is interesting when the talk falls on null coalescing operators in Haskell is that there's a monoid over Maybe that corresponds to that. It's called First
, and it returns the leftmost non-Nothing
value from a series of candidates.
For reasons that may be clearer later, I'll use the one from Data.Semigroup
, so
import Data.Semigroup
In order to get the Monoid
behaviour over Maybe
, you need to wrap the First
value in an Option
; e.g.:
*Q49616294> (Option $ Just $ First 42) <> (Option $ Just $ First 1337)
Option {getOption = Just (First {getFirst = 42})}
Granted, that's quite a long-winded way to select the left-most value, but highlights that null coalesce is 'just' a monoid:
*Q49616294> (Option $ Just $ First 42) <> (Option Nothing)
Option {getOption = Just (First {getFirst = 42})}
*Q49616294> (Option Nothing) <> (Option $ Just $ First 1337)
Option {getOption = Just (First {getFirst = 1337})}
Since that's too verbose for practical use, you could decide to write a custom operator that repackages Maybe
values as Option First
values, applies the <>
operations, and then unwraps the result from Option First
back to Maybe
:
(<?>) :: Maybe a -> Maybe a -> Maybe a
mx <?> my =
let ofx = Option $ sequenceA $ First mx
ofy = Option $ sequenceA $ First my
leftmost = ofx <> ofy
in getFirst $ sequenceA $ getOption $ leftmost
While you could write this operator as one big expression, I chose to use the let...in
syntax to 'show my work'.
One problem remains, though:
*Q49616294> Just 42 <?> Just 1337
Just 42
*Q49616294> Nothing <?> Nothing
Nothing
While the operation returns a Just
value as long as at least one of the arguments is a Just
value, it can return Nothing
.
How do you apply a fallback value so that you're guaranteed to get a value in all cases?
You can take advantage of Option
being Foldable
, and then still fold over <>
- only this time, a different Monoid
instance is in use:
(<!>) :: Maybe a -> a -> a
mx <!> y =
let ofx = Option $ sequenceA $ First mx
fy = First y
in getFirst $ foldr (<>) fy ofx
This operator folds over ofx
, using fy
as an initial value. Here, <>
belongs to the First
Semigroup
, which unconditionally returns the leftmost value. There's no Option
involved here, since foldr
peels that layer away. Since we're folding from the right, though, the initial value fy
will always be ignored if ofx
contains a value.
*Q49616294> Just 42 <!> 1337
42
*Q49616294> Nothing <!> 1337
1337
You can now write the desired function as follows:
copyFile :: String -> String -> IO String
copyFile input output = do
text <- readFile $ (maybeFromNull input) <!> defaultInPath
writeFile (maybeFromNull output <!> defaultOutPath) text
return text
It turns out that in this case, you don't even need <?>
, but in other situations, you could use this operator to chain as many potential values as you'd like:
*Q49616294> Just 42 <?> Nothing <?> Just 1337 <!> 123
42
*Q49616294> Nothing <?> Nothing <?> Just 1337 <!> 123
1337
*Q49616294> Nothing <?> Nothing <?> Nothing <!> 123
123
Not only is this way of implementing null coalescing behaviour needlessly complicated, I wouldn't be surprised if it doesn't perform well to add spite to injury.
It does, however, illustrate the power and expressivity of Haskell's built-in abstractions.
Upvotes: 3
Reputation: 531808
Maybe
type constructorFirst, encode your "null" strings correctly using Maybe
. Then, use the maybe
function to return your default values if an argument is Nothing
.
func :: Maybe String -> Maybe String -> IO String
func inFile outFile = do
text <- readFile $ maybe defaultIn id inFile
writeFile (maybe defaultOut id outFile) text
return text
Data.Maybe
If you don't mind an extra import, you can use fromMaybe d = maybe d id
.
import Data.Maybe
func :: Maybe String -> Maybe String -> IO String
func inFile outFile = do
text <- readFile $ fromMaybe defaultIn inFile
writeFile (fromMaybe defaultOut outFile) text
return text
??
yourselfEither way, you can define your own coalescing operator from either function:
?? :: Maybe String -> String -> String
(??) = flip fromMaybe
-- a ?? b = fromMaybe b a
-- a ?? b = maybe b id a
and write
func inFile outFile = do
text <- readFile (inFile ?? defaultIn)
writeFile (outFile ?? defaultOut) text
return text
Maybe
Your four types of calls would look like this, assuming you aren't already
getting the values from a function that returns a Maybe String
value.
func Nothing Nothing
func (Just "input.txt") Nothing
func Nothing (Just "output.txt")
func (Just "input.txt") (Just "output.txt")
Upvotes: 2
Reputation: 123560
If you have a value that may or may not be provided, you should definitely encode this safely and flexibly with Maybe.
However, if you really want to replace an empty string or any other magic value, you can easily use if..then..else
as an expression:
func :: String -> IO ()
func input = do
text <- readFile (if input == "" then defaultIn else input)
putStrLn text
and of course, once you do switch to Maybe and find yourself with a plain string, you can use the same to call it:
func :: Maybe String -> IO ()
func input = do
text <- readFile $ fromMaybe "default.txt" input
putStrLn text
main = do
putStrLn "Enter filename or blank for default:"
file <- getLine
func (if file == "" then Nothing else Just file)
Upvotes: 1