Bercovici Adrian
Bercovici Adrian

Reputation: 9360

Coalesce operator alternative in Haskell

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

Answers (3)

Mark Seemann
Mark Seemann

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

chepner
chepner

Reputation: 531808

Use the Maybe type constructor

First, 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

Using 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

Defining ?? yourself

Either 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

Using 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

that other guy
that other guy

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

Related Questions