Linear types are a recent addition to Haskell that enables finer-grained control over resource management, ensuring that values with linear types are used exactly once. This can help prevent sharing and duplication when it’s important, for example, in managing resources like file handles or memory allocation, or when writing concurrent code.
In a language with linear types, a value is linear if it is used exactly once in the code. This property can ensure that the runtime system does not need to create multiple copies of the same value, which can be useful for performance and, as mentioned earlier, managing resources safely.
Linear types were introduced in Haskell through the "Linear Haskell" extension, which notably added the ‘Data.Unrestricted.Linear‘ module, providing the tools and utilities to work with linear types. To enable Linear Haskell, you need to add the ‘LinearTypes‘ language pragma:
{-# LANGUAGE LinearTypes #-}
Now, let’s look at an example. Suppose we are working with file handles. We want to be sure that a file is only read once to prevent possible inconsistencies in case of multiple reads happening concurrently. Here’s a simplified IO API with a linear read function:
{-# LANGUAGE LinearTypes #-}
module LinearFileIO where
import qualified Data.Unrestricted.Linear as U
import qualified System.IO as S
data File
data CloseResult
open :: S.FilePath -> S.IOMode -> IO (U.Unrestricted File)
close :: File ->. CloseResult
readFileContents ::
(String ->. U.Unrestricted a) ->
File ->
. U.Unrestricted a
In this example, ‘File‘ and ‘CloseResult‘ are abstract data types representing file handles and the result of closing a file, respectively.
Notice the ‘->.‘ arrow instead of the usual ‘->‘. This means that the function is linear and consumes its argument exactly once. Thus, the ‘readFileContents‘ function enforces that a linear function (given by ‘(String ->. U.Unrestricted a)‘) is applied to the file content only once, and the ‘File‘ type is taken by this function linearly as well.
Here’s a simple program that uses our linear file IO:
{-# LANGUAGE LinearTypes #-}
module Main where
import LinearFileIO
import qualified System.IO as S
import qualified Data.Unrestricted.Linear as U
printFile :: S.FilePath -> IO ()
printFile path = do
U.Unrestricted file <- open path S.ReadMode
let onClose contents = close file
withContents onClose (readFileContents U.fromUnrestricted) file
return ()
-- withContents: a higher order function that applies a linear function on the contents
withContents ::
(String ->. CloseResult) ->
(String ->. U.Unrestricted a) ->
File ->
. U.Unrestricted a
withContents close f file = readFileContents (str -> f str `U.lseq` close str) file
In this example, we open a file and read its contents using the linear ‘readFileContents‘ function. The ‘withContents‘ function applies a linear function ‘f‘ to the file contents and then closes the file using the ‘onClose‘ function. The ‘U.lseq‘ function from the ‘Data.Unrestricted.Linear‘ module is used to sequence actions in a linear context, ensuring that resource management is properly done.
To summarize, linear types in Haskell provide stronger guarantees about value usage, preventing needless duplication and ensuring safer resource management. The Linear Haskell extension introduces the necessary utilities and syntax to work with linear types and functions, improving the expressiveness and safety of the language.