Skip to content

Commit

Permalink
Merge pull request #22 from TheInnerLight/development
Browse files Browse the repository at this point in the history
Development
  • Loading branch information
TheInnerLight authored Sep 4, 2016
2 parents 6abe275 + 9fd5acc commit 630144d
Show file tree
Hide file tree
Showing 28 changed files with 1,148 additions and 552 deletions.
1 change: 1 addition & 0 deletions NovelIO.sln
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "content", "content", "{8E6D
docs\content\index.fsx = docs\content\index.fsx
docs\content\motivation.fsx = docs\content\motivation.fsx
docs\content\oopintro.fsx = docs\content\oopintro.fsx
docs\content\operators.fsx = docs\content\operators.fsx
docs\content\pickler.fsx = docs\content\pickler.fsx
docs\content\tutorial.fsx = docs\content\tutorial.fsx
EndProjectSection
Expand Down
7 changes: 7 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
#### 0.4.0-alpha - 04/09/2016
API improvements, particularly centred around simplifying common tasks
Added support for asynchronous IO within the IO type
Added support for forking and awaiting TPL tasks
Many performance optimisations
Documentation massively improved

#### 0.3.0-alpha - 12/07/2016
Added support for recursive PUs
Renamed Handles to Channels - part of wider API improvements
Expand Down
17 changes: 8 additions & 9 deletions docs/content/channels.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,14 @@ An example is the `File.withTextChannel` function:
*)

io {
let withChannelOR = File.withTextChannel FileMode.Open FileAccess.Read
return withChannelOR (File.assumeValidFilename "test.txt") (fun channel ->
io {
let! l1 = TextChannel.getLine channel
let! l2 = TextChannel.getLine channel
return l1, l2
})
}

File.withTextChannel File.Open.defaultRead (File.Path.fromValid "test.txt") (fun channel ->
io {
let! l1 = TextChannel.getLine channel
let! l2 = TextChannel.getLine channel
return l1, l2
})


(**
Expand Down
18 changes: 8 additions & 10 deletions docs/content/files.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ The first method of creating `Filepath`s is to use active patterns on a string,
*)

match "test.txt" with
|ValidFilename fName -> Some fName // do something with the valid filename
|InvalidFilename -> None // handle the invalid filename case
|ValidFilePath fName -> Some fName // do something with the valid filename
|InvalidFilePath -> None // handle the invalid filename case

(**
If we know that a conversion to a `Filepath` is definitely going to be succesful, we can instead use `File.assumeValidFilename`
*)

let fName = File.assumeValidFilename "test.txt"
let fName = File.Path.fromValid "test.txt"

(**
Should we be mistaken about the supplied string being a valid filename, an `ArgumentException` will be thrown.
Expand All @@ -37,23 +37,21 @@ The `File` modules contains functions very similar to `System.IO.File` defined i
*)

io {
let! lines = File.readLines fName
let! lines = File.readAllLines fName
return lines
}

(**
## File Channels
If you need more fine-grained control over File IO, the way to achieve this is using Channels. Text and Binary Channels (`TChannels` and `BChannels`) support explicit reading and writing of their corresponding datatype.
*)

let readLines file =
io {
let withChannelOR = File.withTextChannel FileMode.Open FileAccess.Read
return! withChannelOR file (fun channel ->
IO.Loops.untilM (TextChannel.isEOS channel) (TextChannel.getLine channel))
}
let readFileUntilEnd path =
File.withTextChannel File.Open.defaultRead path (fun channel ->
IO.Loops.untilM (TextChannel.isEOF channel) (TextChannel.getLine channel))

(**
Expand Down
152 changes: 129 additions & 23 deletions docs/content/index.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,38 @@
#r "NovelIO/NovelIO.dll"
open NovelFS.NovelIO

let someAction = IO.return' ()

(**
Introduction
======================
NovelIO is a library designed to bring the explicit safety and robustness of Haskell's IO monad to the .NET framework. The result is a purely functional approach to describing I/O operations whereby the application of functions does not perform side-effecting computations but rather constructs a data type representing a sequence of actions that can later be executed.
NovelIO is a library designed to bring the explicit safety and robustness that comes with describing effects in the type system to the .NET framework.
The result is a purely functional approach to describing I/O operations whereby functions do not perform side-effects but rather construct values that represent a sequence of effects that can later be executed.
The primary goal of this library is to help developers to design more maintainable and testable code by making it easier to reason about when I/O operations occur.
Much like in Haskell, we introduce the `IO<'a>` type which represents some action that, when performed successfully, returns some result `'a.` Here are some examples:
* An IO action that prints the string "Hello World" to the screen has type `IO<unit>`.
* An IO action that gets a line of text from the Console has type `IO<string>`.
* An IO action that opens a TCP connection has type `IO<TCPConnectedSocket>`.
* An IO action that launches some missiles has type `IO<unit>`
The IO action can equally represent a sequence of actions:
The IO action can equally represent an arbitrary sequence of actions:
* An IO action that requests a Name, then that person's Date of Birth from a service might have type `IO<string, DateTime>`
* An IO action that returns a potentially unknown number of lines from a file might have type `IO<string list>`
Indeed an entire web server could be represented as a single value of type `IO<unit>`!
The key distinction between this representation of effects and the side-effects found in imperative programming is that values of type `IO<'a>` do not represent the result of some side effect, they actually represent the action/effect that can be `run` to produce a particular result.
Values of type `IO<'a>` are distinct from traditional values in that they do not represent the result of some side effect, they rather represent an action (or sequence of actions) that can be `run` to produce a particular result.
The idea of actions being values rather than functions is extremely powerful, it allows us to begin with a small set of orthogonal primitive actions that can be passed around and composed, using combinator functions, to build up to advanced behaviours quickly, easily and safely!
## Running IO Actions
`IO<'a>` Actions can be `run` using the `IO.run` function. This results in all of the side-effects being evaluated, resulting in something of type `'a`.
`IO<'a>` Actions can be `run` using the `IO.run` function. This results in all of the side-effects being evaluated and the generation of a result of type `'a`.
These values can then be re-used and run again to evaluate the side-effects once more.
Expand All @@ -49,7 +60,7 @@ printfn "%s" (IO.run exmpl2)

(**
If we run these examples, we can note the different behaviour.
Take careful note of the different behaviour.
In the first example `exmpl` represents the result of the user input from the console, we perform that side effect only once and print the same value to the console twice.
Expand All @@ -59,35 +70,129 @@ In the second example `exmpl2` represents the action of reading user input from
It is possible (albeit certainly not recommended!) to call `IO.run` on every small block of IO code. It is also possible to call `IO.run` only once, in the main function, for an entire program: it is then possible to visualise the running of a program as the only effectful part of otherwise pure, referentially transparent code.
## Sequencing IO Actions
## Compositionality
One of the key attractions of this representation of IO is that we can design IO actions and then compose them together to build new and more complex IO actions.
Let's assume for a moment that we wish to read two lines from the console and return them as a tuple. That can be achieved as follows:
*)

let readTwoLines =
io {
let! line1 = Console.readLine
let! line2 = Console.readLine
return line1, line2
}

(**
`let!` is used to request the result of an IO action when the enclosing action is run.
Notice that we have taken two primitive IO actions of type `IO<string>` and used them to construct a new IO action of type `IO<string*string>`
Likewise, if we wished to write two lines to the console, we could construct an action like this:
*)

let writeTwoLines line1 line2 =
io {
do! Console.writeLine line1
do! Console.writeLine line2
}

(**
`do!` is used to evaluate an IO action of type `unit` when the enclosing action is run.
In this case, we have taken two IO actions of type `IO<unit>` and created a new action of type `IO<unit>`.
## Loops
A common task during I/O operations is to perform some action until a condition is met. There are a variety of combinators to help with this sort of task:
Let's assume we wish to print the numbers 1..10 to the console. One way of doing this would be:
*)

let print1To10 =
IO.iterM (fun i -> Console.writeLine <| string i) [1..10] // The lambda here could be replaced by (Console.writeLine << string)

(**
It is possible to sequence I/O operations using the `io` computation expression (very similar to the do notation found in Haskell).
The `iterM` function is used to define `for` loops in the IO monad. The below code is completely equivalent to the above:
*)

let print1To10For =
io {
for i in [1..10] do
do! Console.writeLine <| string i
}

(**
A common task in File IO is performing a loop to retrieve lines from a file until you reach the end of the file.
In this case, we can't use a simple `for` loop as we did previously because the logic for checking the loop end condition is also side effecting! Fortunately, we have another function for this occassion:
*)


let readFileUntilEnd path =
File.withTextChannel File.Open.defaultRead path (fun channel ->
IO.Loops.untilM (TextChannel.isEOF channel) (TextChannel.getLine channel))

(**
The `withTextChannel` encapsulates the lifetime of the text channel, accepting as an argument a function where we make use of the channel.
In this function, we use the `untilM` combinator, its first argument is an `IO<bool>` condition and its second is an action to perform while the condition is `false`.
It runs a `list` of all the results we generated from the action argument while the condition was `false`.
## Parallel and Asychronous IO
### Forking IO actions
If you wish to perform some IO on another thread then `forkIO` is the function of choice. It simply performs the work on the .NET thread pool and doesn't ever return a result.
*)

io {
let! l1 = Console.readLine
let! l2 = Console.readLine
let! l3 = Console.readLine
do! IO.putStrLn <| sprintf "You entered: %A" [l1; l2; l3]
} |> IO.run
do! IO.forkIO someAction
}

(**
If you wished to perform a task and then retrieve the results later, you would need to use `forkTask` and `awaitTask`.
*)

io {
let! task = IO.forkTask <| IO.replicateM Random.nextIO 100 // create a task that generates some random numbers on the thread pool
let! results = IO.awaitTask task // await the completion of the task (await Task waits asychronously, it will not block threads)
return results
}

(**
Here we simply read three lines from the console and print the result back to the console as a list.
Needless to say, highly complex actions can be built up in this way. For example, running a webserver could be represented as a single `IO` action.
### Parallel actions
Entire lists of IO actions can be performed in parallel using the functions in the `IO.Parallel` module. This gives us very explicit, fine-grained, control over what actions should take place in parallel.
## Parallel IO
In order to execute items in parallel, we can simply build a list of the IO actions we wish to perform and use the `IO.Parallel.sequence` combinator.
IO actions can also be performed in parallel using the `IO.parallel` combinators. This gives us very explicit, fine-grained, control over what actions should take place in parallel.
> **Aside:** You may notice that `IO.Parallel.sequence` has exactly the same type signature as the `IO.sequence` function.
These two functions are fundamentally very similar, the only difference is that `IO.sequence` joins a list of actions sequentially and `IO.Parallel.sequence` joins a list of actions in parallel.
In order to execute items in parallel, we can simply build a list of the IO actions we wish to perform and use the `par` combinator. For example:
For example:
*)

io {
let fName = File.assumeValidFilename "file.txt"
let! channel = File.openTextChannel FileMode.Open FileAccess.Read fName
return IO.Parallel.par [Console.readLine; TextChannel.getLine channel]
let fName = File.Path.fromValid "file.txt"
let! channel = File.openTextChannel File.Open.defaultRead fName
return IO.Parallel.sequence [Console.readLine; TextChannel.getLine channel]
} |> IO.run

(**
Expand All @@ -98,14 +203,15 @@ This describes a program that gets a line from the console and a line from a spe
It's very likely that the set of functions included in this library will not cover every possible IO action we might ever wish to perform. In this case, we can use the `IO.fromEffectful` to take a non-referentially transparent function and bring it within IO.
If we decide to create an action that exits the program, this could be accomplished as follows:
Here is an example of creating a simple IO action that increments a reference variable.
*)

let exit = IO.fromEffectful (fun _ -> System.Environment.Exit 0)
let num = ref 0
let incrIO = IO.fromEffectful (fun _ -> incr num)

(**
This should allow us to construct arbitrary programs entirely within IO.
This allows us to construct arbitrary programs entirely within IO.
*)
49 changes: 1 addition & 48 deletions docs/content/motivation.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Consider:
*)

let x = 2*2
let x = 2 * 2

(**
Expand Down Expand Up @@ -177,52 +177,5 @@ io {
So, using this approach we can easily describe either behaviour while still keeping the intent clear and explicit.
## Lazy evaluation and exceptions
This example is more or less taken from Erik Meijer's Curse of the excluded middle (https://queue.acm.org/detail.cfm?ref=rss&id=2611829)
Consider the following code where we try to combine lazy evaluation with File IO:
*)

let floatLines =
try
System.IO.File.ReadLines("testfile.txt")
|> Seq.map (float) // parse each line as a float
with
| _ -> Seq.empty

Seq.iter (printfn "%f") floatLines // print each float to the console

(**
This code appears to be relatively safe - we have a comforting `try`/`with` block around a function that may fail at runtime. This code, however, does not function in the way it immediately appears to.
In reality, the map is not actually evaluated until we enumerate the sequence with `Seq.iter`, this means that any exception, if triggered, will actually be thrown outside the `try`/`with` block causing the program to crash.
Consider an alternative using NovelIO's expression of IO:
*)

let fName = File.assumeValidFilename "testfile.txt"

let fileIO = io {
let! lines = File.readLines fName // sequence of io actions which each read a line from a file
let! floatLines = IO.mapM (IO.map float) lines // parse each line, collecting the results
do! IO.iterM (IO.putStrLn << sprintf "%f") floatLines // print each float to the console
}

try
IO.run fileIO // side effects occur *only* on this line
with
|_ -> () // error case


(**
This code describes exactly the same problem but we know that side-effects can occur in exactly one place `IO.run`. That means that success or failure need be handled in only that one location. We can therefore design complicated programs where IO is described using pure, referentially transparent functions and potentially error-prone behaviour is made very explicit and side-effects are restricted to very specific and obvious locations.
Hopefully this demonstrates how being explicit about when effects occur can massively improve the ability of developers to understand and reason about their code.
*)

Loading

0 comments on commit 630144d

Please sign in to comment.