{-- -- TalTech ITI0212 -- -- Functional Programming -- -- week 7, 2022-03-09 -- -- Monadic I/O --} module Lecture7 import Data.String -- for parsing numbers import System.File -- for file I/O {-- Executing Statements vs Evaluating Expressions --} -- In imperative programming languages executing a statement -- causes a change of state. -- In purely functional programming languages we don't have statements, -- only expressions. -- Evaluating an expression does not cause anything to happen. -- So how can we make things happen in a functional language? {-- The IO Type Constructor --} -- The IO type constructor is used to signify *computations*. -- These are expressions that act as instructions to the run-time system. -- When a computation is *run* it may perform *actions* -- that interact with the world. -- An expression of type IO a represents computation -- that when run performs some actions -- and then returns a result of type a. -- The run-time system includes some basic computations -- for doing console, file, and network IO. -- the hello world computation: greet : IO Unit greet = putStrLn "hello world!" -- In order to run a computation and perform its actions -- we must compile or :exec it. {-- Monadic Combinators --} -- We build up compound computations using two *monadic combinators*: -- -- * pure : a -> IO a -- This creates a trivial computation -- that performs no actions and returns the given value. -- -- * (>>=) : IO a -> (a -> IO b) -> IO b -- This sequences computations by running first one -- and passing the resulting value to the next one. -- the hello world computation using the monadic combinators: greet' : IO Unit greet' = pure "hello world!" >>= putStrLn -- greet the user by name: meet_and_greet : IO Unit meet_and_greet = putStr "What's your name? " >>= const getLine >>= \ name => putStrLn ("Hello " ++ name ++ "!") {-- Do Notation --} -- There is syntactic sugar for writing sequences of computations -- in an imperative looking style. meet_and_greet' : IO Unit meet_and_greet' = do putStr "What's your name? " name <- getLine putStrLn ("Hello " ++ name ++ "!") -- This may look like a block of imperative statements, but it is not. -- In a purely functional programming language there are no statements! -- Do notation is just syntactic sugar: -- -- do -- x <- computation1 -- y <- computation2 -- z <- computation3 -- ... -- -- is syntax for: -- computation1 >>= -- \ x => computation2 >>= -- \ y => computation3 >>= -- \ z => ... komp_do : (f : a -> IO b) -> (g : b -> IO c) -> (x : a) -> IO c komp_do f g x = do y <- f x g y komp_bind : (f : a -> IO b) -> (g : b -> IO c) -> (x : a) -> IO c komp_bind f g x = f x >>= g -- komp looks like a kind of "funny composition". {-- Returning Results from Computations --} -- Let's write a computation that gets a list of numbers form the user: public export get_numbers : IO (List Integer) get_numbers = do putStr "Please enter a number: " str <- getLine case parseInteger str of Nothing => pure [] Just num => do nums <- get_numbers pure (num :: nums) -- By default :exec doesn't display the results of running a computation. -- We can use the functions print or printLn to do so. {-- File I/O --} -- read the contents of a text file: read_file : (path : String) -> IO (Either FileError String) read_file path = do result <- openFile path Read case result of Left error => pure $ Left error Right file => do result <- fRead file closeFile file case result of Left error => pure $ Left error Right text => pure $ Right text -- There is a convenieng function in System.File.ReadWrite called readFile -- that does this. -- In a few weeks we will learn a more elegant way to structure computations.