Before you begin
Why this matters
When a program runs with the wrong setting, the first question is: where did this value come from? If you can follow the order in which your code chooses, you can answer that by reading it, and you can spot the expressions that replace what someone typed without reporting an error.
What this helps you reason about
- Predict which place a setting comes from when several disagree.
- Spot a parse failure that looks like a missing value and lets a lower place win.
- Return an error that says which place held the bad value.
A program often needs one setting, such as a port number, that can come from several places. This lesson builds the part of a Haskell program that picks one value, checks it, and says where it came from.
Already know this? The short version
- Take the setting from the highest source that is present, here flag, then environment variable, then file, then the default.
- Hold each source as a Maybe String; Just "" is present, and lookupEnv and lookup return it for a variable set to the empty string.
- Choose first, then parse: (flag >>= readMaybe) <|> (env >>= readMaybe) turns a bad flag into Nothing and lets env win.
- Check the chosen text with your own rule (here 1 to 5 ASCII digits, then 1 to 65535), because readMaybe accepts " 80 ", "0x50", and "(80)"; return Left with the source and the text when it fails.
- Read getArgs, lookupEnv, and files in IO, in main, and pass their text to a pure function.
Words in this lesson (9)
- setting
- One value a program needs before it can do its work, such as the port number it listens on.
- source
- A place a setting can come from: a command-line flag, an environment variable, a configuration file, or a default written in the program.
- precedence
- The fixed order that decides which source wins when several supply the same setting. In this lesson: flag, then environment variable, then file, then default.
- present and absent
- A source is present when it supplies a value for the setting, even an empty one. It is absent when it supplies nothing at all.
- Maybe
- A type whose values are Just x, holding a value, or Nothing, holding none. Maybe Int, for example, is a number that may be missing.
- equation
- One line of a function definition, such as countdown 0 = putStrLn "liftoff". GHC tries a function’s equations from top to bottom and uses the first one that matches.
- Either
- A type whose values are Left e or Right x. By convention Left holds an error and Right holds a successful result.
- validation
- Checking that a value follows the rules the program needs, such as “1 to 5 digits, from 1 to 65535”, before using it.
- provenance
- A record of where a value came from: here, the name of the source that won, kept with the value and named in every error.
Build brief · one port setting
- Input
- The text of a flag, an environment variable, and a file line, each of which may be absent.
- Output
- A port number and the source it came from, or an error that names the source and its text.
- Policy
- flag, then env, then file, then the default 3000. In this program’s policy, a source that is set counts even when its value is empty. Only the winner is checked: 1 to 5 digits, 1 to 65535. An invalid winner is an error.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Precedence
Which source wins?
A setting is one value a program needs before it can do its work, such as the port number it listens on. A setting can come from several sources: a command-line flag such as --port=7000, typed after the program’s name; an environment variable such as APP_PORT, set before the program starts; a line in a configuration file; or a default written in the program.
When several sources supply the port, the program needs a rule. Precedence is that rule: a fixed order of sources in which the first one that supplies a value wins. This program uses a common order: flag, then environment variable, then file, then the default 3000.
For now, the sources are arguments of type Maybe String: Just "7000" when the source supplies text, Nothing when it is absent. GHC tries the equations of choose from top to bottom, so their order is the precedence. _ matches any value.
choose :: Maybe String -> Maybe String -> Maybe String -> (String, String)
choose (Just raw) _ _ = ("flag", raw)
choose _ (Just raw) _ = ("env", raw)
choose _ _ (Just raw) = ("file", raw)
choose _ _ _ = ("default", "3000") Write the sources in precedence order; the first equation that matches is the source that wins.
Precisely: the order is a choice
No language or operating system sets this order. Flag, then environment, then file, then default is a common convention, because each source is more specific to one run than the next. Some tools add more sources, such as a file per user and a file per project. Whatever the order, a program should write it down where its users can read it.
Experiment
Follow the sources from the highest
GHC tries the equations of choose from top to bottom and uses the first one that matches. The first matches only when flag is a Just, the second only when env is, so the order of the equations is the precedence.
Why it matters When a setting surprises you, follow the same order by hand. The first source that is present is the one that won.
Predict which equation of choose is used before running. Then change (Just "5000") in main to Nothing, so the environment variable is absent too, and predict again.
choose :: Maybe String -> Maybe String -> Maybe String -> String
choose (Just raw) _ _ = raw ++ " from flag"
choose _ (Just raw) _ = raw ++ " from env"
choose _ _ (Just raw) = raw ++ " from file"
choose _ _ _ = "3000 from default"
main :: IO ()
main = do
let port = choose Nothing (Just "5000") (Just "4100")
putStrLn port
What to look for
GHCi shows one location per stop, not a stack of calls. port starts unevaluated: it is computed when putStrLn needs it. Then GHCi stops in choose on line 3, the env equation, because the first equation needs flag to be a Just. raw shows as unevaluated there. The output appears when putStrLn writes it. After the edit, the stop in choose is on line 4, the file equation, and the run prints 4100 from file.
Expected output
5000 from env02 / Presence
Is an empty value the same as a missing one?
A source is present when it supplies a value, and absent when it supplies nothing. Starting a program with APP_PORT set to "" sets the variable, to an empty value. In this program’s policy, a source that is set counts even when its value is empty: someone set it, so the program checks that empty value and reports an error, instead of moving on to the next source. Some tools choose the other policy and treat an empty variable as not set; either is fine if the documentation says which.
Maybe keeps the two apart: Just "" is present and empty, Nothing is absent. lookupEnv "APP_PORT" from System.Environment returns exactly that. a <|> b, from Control.Applicative, keeps a when it is a Just, even Just "", and gives b only when a is Nothing.
env <- lookupEnv "APP_PORT" -- Just "" when set to ""
let chosen = env <|> Just "3000" -- stays Just "" Just "" is a value, not a missing one. Choose with <|>.
Coming from JavaScript: <|> on Maybe behaves like ?? rather than ||: Just "" counts as present.
Precisely: setEnv and the empty string
System.Environment.setEnv name "" does not set the variable to "": for compatibility it removes it, as unsetEnv does. That is why the experiment models the environment with a list, the shape getEnvironment returns, instead of setting APP_PORT inside the program.
Experiment
Tell an empty value from a missing one
lookup returns Just the value when the name is in the list, even when the value is "", and Nothing when it is not. a <|> b keeps a when a is a Just, and gives b only when a is Nothing.
Why it matters Keep each source as a Maybe and choose with <|>. Check the chosen text afterwards.
Predict the printed value before running. Then change [("APP_PORT", "")] to [] and predict again.
import Control.Applicative ((<|>))
-- Stands in for getEnvironment: a list of (name, value) pairs.
environment :: [(String, String)]
environment = [("APP_PORT", "")]
main :: IO ()
main = do
let port = lookup "APP_PORT" environment <|> Just "3000"
print port
What to look for
port starts unevaluated; GHCi stops in port, and then in environment, when lookup needs the list. The run prints Just "": lookup found the name, so <|> keeps that result and never uses Just "3000". After the edit, the run prints Just "3000".
Expected output
Just ""03 / Validation
What if the winning value is not a port?
The chosen value is still text. Before the program uses it as a port, validation checks the text against the program’s own rule: one to five ASCII digits (the characters 0 to 9), and a number from 1 to 65535. Leading zeros are allowed, so 00080 means 80.
readMaybe, from Text.Read, is not that rule. It gives Nothing for "80x" and "+80", but it accepts " 80 " with spaces, "0x50" (80 in hexadecimal), and "(80)". So the program checks the characters with all isDigit first; isDigit from Data.Char is true for the ASCII digits only. Then read converts.
Parse after choosing, not before. (flag >>= readMaybe) <|> (env >>= readMaybe) parses each source first, and a failed parse is Nothing, which looks exactly like an absent flag, so env wins. This program chooses with choose, checks the chosen text, and returns an error when it fails.
load flag env file
| null raw || length raw > 5 || not (all isDigit raw) =
Left (source ++ " " ++ show raw ++ " is not 1 to 5 digits")
| port < 1 || port > 65535 =
Left (source ++ " " ++ show raw ++ " is not between 1 and 65535")
| otherwise = Right (port, source)
where
(source, raw) = choose flag env file
port = read raw Choose first, then check. A failed check is an error, not a reason to try the next source.
Coming from Python: readMaybe returns Nothing instead of raising an error, so a failure is easy to mistake for a missing value.
Precisely: when read runs
port = read raw is computed only when a later guard needs it. The first guard runs first, so read only ever sees one to five ASCII digits, which it always converts. Without that guard, read "80x" would stop the program with Prelude.read: no parse.
Experiment
Give the parser a value that is not a port
flag >>= readMaybe parses the flag if there is one. When parsing fails, the result is Nothing, which looks exactly like a missing flag, so <|> moves on to env. The edit chooses the text first and parses it once, so a bad flag stays a failure.
Why it matters Choose first, then parse. Parsing before choosing turns an invalid value into a missing one.
Predict the printed value before running. Then replace the line under load flag env = with (flag <|> env <|> Just "3000") >>= readMaybe and predict again.
import Control.Applicative ((<|>))
import Text.Read (readMaybe)
load :: Maybe String -> Maybe String -> Maybe Int
load flag env =
(flag >>= readMaybe) <|> (env >>= readMaybe) <|> Just 3000
main :: IO ()
main = print (load (Just "80x") (Just "5000"))
What to look for
The first stops in load show flag and env unevaluated. Then there is a stop that lists only flag, and later one that lists only env: env was needed, because readMaybe gave Nothing for "80x". The run prints Just 5000, the environment’s port, although a flag was given. After the edit, no stop lists env alone, and the run prints Nothing.
Expected output
Just 500004 / Provenance
How does the program say where a value came from?
Provenance is a record of where a value came from. The program keeps it by returning the source with the port, and by putting the source and the raw text in every error. Then error: flag "80x" is not 1 to 5 digits tells the person which of the four places to fix.
load returns Either String (Int, String): Left with a message, or Right with the port and the source’s name. show raw puts the raw text in quotes, so an empty value shows as "". describe turns either kind of result into one line.
describe :: Either String (Int, String) -> String
describe (Left message) = "error: " ++ message
describe (Right (port, source)) = show port ++ " from " ++ source Return the source with the value, and name the source and the raw text in every error.
Precisely: showing every setting at start
Many tools can print each setting with the place it came from; git config --list --show-origin is one example. A function that already returns the source makes that a few lines of code.
05 / Real sources
Where do the real flags, variables, and files come in?
A real program reads its sources from outside, in IO. getArgs gives the words of the command line, lookupEnv reads an environment variable, and readFile reads a file.
Keep those reads in main. main collects the text and passes it to load; load is a pure function that reads nothing from outside. Then you can call load with any combination of sources, as the experiments and the program on this page do.
The stepper on this page always starts a program without arguments, and with an environment you cannot set from outside. So this lesson passes the sources as Maybe values: the same shape lookupEnv gives a real program.
-- The start of a real program; not run on this page.
main :: IO ()
main = do
args <- getArgs
env <- lookupEnv "APP_PORT"
let flags = [drop 7 arg | arg <- args, isPrefixOf "--port=" arg]
flag = if null flags then Nothing else Just (last flags)
putStrLn (describe (load flag env Nothing)) Read the command line, the environment, and files in IO, in main, and pass plain values to the pure code that decides.
Precisely: the command line
[drop 7 arg | arg <- args, … ] is a list comprehension: it keeps each argument that starts with --port= and drops those seven characters. getArgs does not include the program’s name. A real tool usually uses a library such as optparse-applicative. The file is passed as Nothing here; reading one is a separate topic in this module, Files, parsing, and serialization.
06 / Try it
Can you predict the winner and the outcome?
The workbench below is a model of this lesson’s policy that runs in your browser. It does not run Haskell code or read your environment. It draws the sources in the reverse of this lesson’s order: flag, the highest source, is at the right end. So the winner is the source furthest to the right that is present.
Predict the result before each change: turn off flag; turn off env as well; turn flag back on and clear its text; then type broken into file while flag holds 7000.
The flag is absent, APP_PORT is set to 0080, and the file says 70000. What does the program report, and why?
Reveal the reasoning
80 from env. The environment variable is the highest source that is present. 0080 is four digits, and leading zeros are allowed, so it gives 80. The file’s 70000 would fail the range check, but the file did not win, so it is never checked.
07 / Whole program
Does the whole program follow the policy?
The complete program below joins the pieces and runs the thirteen acceptance cases, one output line each. Each case gives the flag, the environment variable, and the file, with Nothing for an absent source. Compile it with GHC and run it to see the same thirteen lines. In main, \(flag, env, file) -> … is a function that takes one case apart, and mapM_ runs it for every case.
import Data.Char (isDigit)
choose :: Maybe String -> Maybe String -> Maybe String -> (String, String)
choose (Just raw) _ _ = ("flag", raw)
choose _ (Just raw) _ = ("env", raw)
choose _ _ (Just raw) = ("file", raw)
choose _ _ _ = ("default", "3000")
load :: Maybe String -> Maybe String -> Maybe String -> Either String (Int, String)
load flag env file
| null raw || length raw > 5 || not (all isDigit raw) =
Left (source ++ " " ++ show raw ++ " is not 1 to 5 digits")
| port < 1 || port > 65535 =
Left (source ++ " " ++ show raw ++ " is not between 1 and 65535")
| otherwise = Right (port, source)
where
(source, raw) = choose flag env file
port = read raw
describe :: Either String (Int, String) -> String
describe (Left message) = "error: " ++ message
describe (Right (port, source)) = show port ++ " from " ++ source
-- Each case is (flag, env, file); Nothing means absent.
cases :: [(Maybe String, Maybe String, Maybe String)]
cases =
[ (Just "7000", Just "5000", Just "4100")
, (Nothing, Just "5000", Just "4100")
, (Nothing, Nothing, Just "4100")
, (Nothing, Nothing, Nothing)
, (Just "", Just "5000", Nothing)
, (Just "80x", Just "5000", Nothing)
, (Just " 80", Nothing, Nothing)
, (Just "+80", Nothing, Nothing)
, (Just "0", Nothing, Nothing)
, (Just "65536", Nothing, Nothing)
, (Just "7000", Nothing, Just "broken")
, (Just "00080", Nothing, Nothing)
, (Nothing, Just "", Just "4100")
]
main :: IO ()
main = mapM_ (\(flag, env, file) -> putStrLn (describe (load flag env file))) cases
Acceptance cases
- flag 7000, env 5000, file 41007000 from flag
- no flag; env 5000, file 41005000 from env
- only file 41004100 from file
- no flag, env, or file3000 from default
- flag "" (set, but empty), env 5000error: flag "" is not 1 to 5 digits
- flag 80x, env 5000error: flag "80x" is not 1 to 5 digits
- flag " 80" (a space first)error: flag " 80" is not 1 to 5 digits
- flag +80error: flag "+80" is not 1 to 5 digits
- flag 0error: flag "0" is not between 1 and 65535
- flag 65536error: flag "65536" is not between 1 and 65535
- flag 7000, file broken7000 from flag; the file is never checked
- flag 0008080 from flag
- env "" (set, but empty), file 4100error: env "" is not 1 to 5 digits
What does this program settle, and what does it leave out?
Reveal the reasoning
It settles one setting: which source wins, whether its text is a port, and how to say where it came from. It does not read a real command line, environment, or file, check settings that did not win, or start anything on the port. Each of those is a separate piece with its own rules.
Practice tickets
- CFG-101 · Stop hiding a bad retention setting → Free
- CFG-102 · Let the last width win → Sign in · Pro
- CFG-103 · Choose the watering minutes → Sign in · Pro
Keep exploring
I/O, buffering, and external state →