packages feed

linear-base-0.2.0: examples/Simple/FileIO.hs

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LinearTypes #-}
{-# LANGUAGE QualifiedDo #-}
{-# LANGUAGE RebindableSyntax #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeInType #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}

-- |
-- Module      : FileIO
-- Description : The Linear File IO example from the Linear Haskell paper.
--
-- We implement a function that prints the first line of a file.
--
-- We do this with the normal file IO interface in base and the linear file IO
-- interface in linear-base. With the latter, the protocol for using files is
-- enforced by the linear type system. For instance, forgetting to close the file
-- will induce a type error at compile time. That is, typechecking proves that all
-- opened files are closed at some later point in execution. With the former
-- interface, we have code that type checks but will error or cause errors at
-- runtime.
module Simple.FileIO where

-- Linear Base Imports
import qualified Control.Functor.Linear as Control
import Control.Monad ()
import Data.Text
import Data.Unrestricted.Linear
import qualified System.IO as System
import qualified System.IO.Resource.Linear as Linear
import Prelude

-- *  Non-linear first line printing

--------------------------------------------

-- openFile :: FilePath -> IOMode -> IO Handle
-- IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode
-- hGetLine :: Handle -> IO String
-- hPutStr :: Handle -> String -> IO ()
-- hClose :: Handle -> IO ()

printFirstLine :: FilePath -> System.IO ()
printFirstLine fpath = do
  fileHandle <- System.openFile fpath System.ReadMode
  firstLine <- System.hGetLine fileHandle
  System.putStrLn firstLine
  System.hClose fileHandle

-- This compiles but can cause issues!
-- The number of file handles you can have active is finite and after that
-- openFile errors. This is especially critical on mobile devices or systems
-- with limited resources.
printFirstLineNoClose :: FilePath -> System.IO ()
printFirstLineNoClose fpath = do
  fileHandle <- System.openFile fpath System.ReadMode
  firstLine <- System.hGetLine fileHandle
  System.putStrLn firstLine

-- This compiles, but will throw an error!
printFirstLineAfterClose :: FilePath -> System.IO ()
printFirstLineAfterClose fpath = do
  fileHandle <- System.openFile fpath System.ReadMode
  System.hClose fileHandle
  firstLine <- System.hGetLine fileHandle
  System.putStrLn firstLine

-- * Linear first line printing

--------------------------------------------

linearGetFirstLine :: FilePath -> RIO (Ur Text)
linearGetFirstLine fp = Control.do
  handle <- Linear.openFile fp System.ReadMode
  (t, handle') <- Linear.hGetLine handle
  Linear.hClose handle'
  Control.return t

linearPrintFirstLine :: FilePath -> System.IO ()
linearPrintFirstLine fp = do
  text <- Linear.run (linearGetFirstLine fp)
  System.putStrLn (unpack text)

{-
    For clarity, we show this function without do notation.

    Note that the current approach is limited.
    We have to make the continuation use the unit type.

    Enabling a more generic approach with a type index
    for the multiplicity, as descibed in the paper is a work in progress.
    This will hopefully result in using

    `(>>==) RIO 'Many a %1-> (a -> RIO p b) %1-> RIO p b`

    as the non-linear bind operation.
    See https://github.com/tweag/linear-base/issues/83.
-}

-- * Linear and non-linear combinators

-------------------------------------------------

-- Some type synonyms
type RIO = Linear.RIO

type LinHandle = Linear.Handle

-- | Linear bind
-- Notice the continuation has a linear arrow,
-- i.e., (a %1-> RIO b)
(>>#=) :: RIO a %1 -> (a %1 -> RIO b) %1 -> RIO b
(>>#=) = (Control.>>=)

infixl 1 >>#= -- same fixity as base.>>=

-- | Non-linear bind
-- Notice the continuation has a non-linear arrow,
-- i.e., (() -> RIO b). For simplicity, we don't use
-- a more general type, like the following:
-- (>>==) :: RIO (Ur a) %1-> (a -> RIO b) %1-> RIO b
(>>==) :: RIO () %1 -> (() -> RIO b) %1 -> RIO b
(>>==) ma f = ma Control.>>= (\() -> f ())

infixl 1 >>== -- same fixity as base.>>=

-- | Inject
-- provided just to make the type explicit
inject :: a %1 -> RIO a
inject = Control.return

-- * The explicit example

-------------------------------------------------

getFirstLineExplicit :: FilePath -> RIO (Ur Text)
getFirstLineExplicit path =
  (openFileForReading path)
    >>#= readOneLine
    >>#= closeAndReturnLine -- Internally uses (>>==)
  where
    openFileForReading :: FilePath -> RIO LinHandle
    openFileForReading fp = Linear.openFile fp System.ReadMode
    readOneLine :: LinHandle %1 -> RIO (Ur Text, LinHandle)
    readOneLine = Linear.hGetLine
    closeAndReturnLine ::
      (Ur Text, LinHandle) %1 -> RIO (Ur Text)
    closeAndReturnLine (unrText, handle) =
      Linear.hClose handle >>#= (\() -> inject unrText)

printFirstLineExplicit :: FilePath -> System.IO ()
printFirstLineExplicit fp = do
  firstLine <- Linear.run $ getFirstLineExplicit fp
  putStrLn $ unpack firstLine