Skip to content
Permalink
Browse files

Pure-functional terminal I/O provides 4 instructions:

1) Write (Out)
2) Write (Err)
3) Read [single character from standard input]
4) ReadLine [line from standard input]

Terminal programs are constructed using LINQ syntax.

For example, the previous program (not pure-functional):

    var x = Console.ReadLine();
    Console.Write(f(x));
    var y = Console.Read();
    var z = Console.Error.Write(y > 10 ? "hi" : "bye");

can be transformed mechanically to the following pure-functional program:

    from x in Terminal.ReadLine
    from _ in Terminal.WriteOut(f(x))
    from y in Terminal.Read
    from z in Terminal.WriteErr(y > 10 ? "hi" : "bye")
    select z

The general rule is that a function that previously returned type T, now returns type Terminal<T>, which can be composed with LINQ. However, critical in this implementation is that *equational reasoning is preserved*. In other words, it is _in fact_ a pure-functional program in that it is as pure-functional as any other pure-functional program.

This implementation has been achieved by using the free monad to encode the effect. This means that this kind of effect environment is incompatible with "the built-in C# modus operandi." Therefore, we introduce a single unsafe function (Terminal#Run) to essentially "fire the missiles." Ideally, a main method would have the type Terminal<Unit>, however, this unsafe interpreter function is provided only for compatibility with a more typical .NET environment. The existence of this function does not falsify the existence of pure-functional Terminal I/O as has been implemented. Indeed, it is a very valuable experiment to attempt to deconstruct a Terminal program in such a way to demonstrate that it violates referential transparency (this will ultimately fail, but the reasons are often highlighted in this exercise).

Issue #4
  • Loading branch information
tonymorris committed Oct 4, 2013
1 parent f22f047 commit a2bc21eb56fea0dde306685944ab672aad0ae9e1
@@ -48,6 +48,7 @@
<Compile Include="src\XSharpx\Tree.cs" />
<Compile Include="src\XSharpx\Iteratee.cs" />
<Compile Include="src\XSharpx\Unit.cs" />
<Compile Include="src\XSharpx\Terminal.cs" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<ProjectExtensions>
@@ -113,6 +113,10 @@ public struct Either<A, B> : IEnumerable<B> {
return Fold(a => a.Left<A, X>().Some(), b => f(b).Select(x => x.Right<A, X>()));
}

public Terminal<Either<A, X>> TraverseTerminal<X>(Func<B, Terminal<X>> f) {
return Fold(a => a.Left<A, X>().TerminalValue(), b => f(b).Select(x => x.Right<A, X>()));
}

public Input<Either<A, X>> TraverseInput<X>(Func<B, Input<X>> f) {
return Fold(a => a.Left<A, X>().InputElement(), b => f(b).Select(x => x.Right<A, X>()));
}
@@ -112,6 +112,10 @@ public struct Input<E> : IEnumerable<E> {
return val.TraverseOption(o => o.TraverseOption(f)).Select(o => new Input<B>(o));
}

public Terminal<Input<B>> TraverseTerminal<B>(Func<E, Terminal<B>> f) {
return val.TraverseTerminal(o => o.TraverseTerminal(f)).Select(o => new Input<B>(o));
}

public Input<Input<B>> TraverseInput<B>(Func<E, Input<B>> f) {
return val.TraverseInput(o => o.TraverseInput(f)).Select(o => new Input<B>(o));
}
@@ -368,6 +368,13 @@ private class ListEnumerator : IEnumerator<A> {
);
}

public Terminal<List<B>> TraverseTerminal<B>(Func<A, Terminal<B>> f) {
return FoldRight<Terminal<List<B>>>(
(a, b) => f(a).ZipWith<List<B>, List<B>>(b, aa => bb => aa + bb)
, List<B>.Empty.TerminalValue()
);
}

public Input<List<B>> TraverseInput<B>(Func<A, Input<B>> f) {
return FoldRight<Input<List<B>>>(
(a, b) => f(a).ZipWith<List<B>, List<B>>(b, aa => bb => aa + bb)
@@ -277,6 +277,15 @@ public struct ListZipper<A> {
select new ListZipper<B>(ll, xx, rr);
}

public Terminal<ListZipper<B>> TraverseTerminal<B>(Func<A, Terminal<B>> f) {
var t = this;
return
from ll in lefts.Reverse.TraverseTerminal(f)
from xx in f(t.focus)
from rr in t.rights.TraverseTerminal(f)
select new ListZipper<B>(ll, xx, rr);
}

public Input<ListZipper<B>> TraverseInput<B>(Func<A, Input<B>> f) {
var t = this;
return
@@ -198,6 +198,10 @@ public struct NonEmptyList<A> : IEnumerable<A> {
return f(head).ZipWith<List<B>, NonEmptyList<B>>(tail.TraverseOption(f), h => t => h & t);
}

public Terminal<NonEmptyList<B>> TraverseTerminal<B>(Func<A, Terminal<B>> f) {
return f(head).ZipWith<List<B>, NonEmptyList<B>>(tail.TraverseTerminal(f), h => t => h & t);
}

public Input<NonEmptyList<B>> TraverseInput<B>(Func<A, Input<B>> f) {
return f(head).ZipWith<List<B>, NonEmptyList<B>>(tail.TraverseInput(f), h => t => h & t);
}
@@ -103,6 +103,10 @@ public struct Option<A> : IEnumerable<A> {
return IsEmpty ? Option<B>.Empty.Some() : f(a).Select(q => q.Some());
}

public Terminal<Option<B>> TraverseTerminal<B>(Func<A, Terminal<B>> f) {
return IsEmpty ? Option<B>.Empty.TerminalValue() : f(a).Select(q => q.Some());
}

public Input<Option<B>> TraverseInput<B>(Func<A, Input<B>> f) {
return IsEmpty ? Option<B>.Empty.InputElement() : f(a).Select(q => q.Some());
}
@@ -0,0 +1,295 @@
using System;

namespace XSharpx {
public struct WriteOp<A> {
private readonly char c;
private readonly A val;
private readonly bool o;

internal WriteOp(char c, A val, bool o) {
this.c = c;
this.val = val;
this.o = o;
}

public Store<char, WriteOp<A>> Character {
get {
var t = this;
return c.StoreSet(x => new WriteOp<A>(x, t.val, t.o));
}
}

public Store<A, WriteOp<A>> Value {
get {
var t = this;
return val.StoreSet(x => new WriteOp<A>(t.c, x, t.o));
}
}

public bool IsOut {
get {
return o;
}
}

public bool IsErr {
get {
return !o;
}
}

public WriteOp<A> SetOut {
get {
return new WriteOp<A>(c, val, true);
}
}

public WriteOp<A> SetErr {
get {
return new WriteOp<A>(c, val, false);
}
}

public WriteOp<A> Redirect {
get {
return new WriteOp<A>(c, val, !o);
}
}

public static WriteOp<A> Out(char c, A val) {
return new WriteOp<A>(c, val, true);
}

public static WriteOp<A> Err(char c, A val) {
return new WriteOp<A>(c, val, false);
}

public WriteOp<B> Extend<B>(Func<WriteOp<A>, B> f) {
var b = f(this);
return new WriteOp<B>(c, b, o);
}

public WriteOp<WriteOp<A>> Duplicate {
get {
return Extend(z => z);
}
}

public Op<A> Op {
get {
return new Op<A>(this.Right<Either<Func<int, A>, Func<string, A>>, WriteOp<A>>());
}
}
}

public static class WriteOpExtension {
public static WriteOp<B> Select<A, B>(this WriteOp<A> k, Func<A, B> f) {
return new WriteOp<B>(k.Character.Get, f(k.Value.Get), k.IsOut);
}
}

public struct Op<A> {
private readonly Either<Either<Func<int, A>, Func<string, A>>, WriteOp<A>> val;

internal Op(Either<Either<Func<int, A>, Func<string, A>>, WriteOp<A>> val) {
this.val = val;
}

public X Fold<X>(Func<Func<int, A>, X> r, Func<Func<string, A>, X> rl, Func<WriteOp<A>, X> p) {
return val.Fold(d => d.Fold(r, rl), p);
}

public bool IsWriteOut {
get {
return val.Any(o => o.IsOut);
}
}

public bool IsWriteErr {
get {
return val.Any(o => o.IsErr);
}
}

public bool IsWrite {
get {
return val.IsRight;
}
}

public bool IsRead {
get {
return val.Swap.Any(o => o.IsLeft);
}
}

public bool IsReadLine {
get {
return val.Swap.Any(o => o.IsRight);
}
}

public Option<WriteOp<A>> WriteOp {
get {
return val.ToOption;
}
}

public Option<char> WriteOpCharacter {
get {
return WriteOp.Select(o => o.Character.Get);
}
}

public Option<A> WriteOpValue {
get {
return WriteOp.Select(o => o.Value.Get);
}
}

public Option<Func<int, A>> ReadValue {
get {
return val.Swap.ToOption.SelectMany(f => f.Swap.ToOption);
}
}

public Option<Func<string, A>> ReadLineValue {
get {
return val.Swap.ToOption.SelectMany(f => f.ToOption);
}
}

public Terminal<A> Lift {
get {
return new Terminal<A>(this.Select(a => Terminal<A>.Done(a)).Right<A, Op<Terminal<A>>>());
}
}

public static Op<A> Read(Func<int, A> f) {
return new Op<A>(f.Left<Func<int, A>, Func<string, A>>().Left<Either<Func<int, A>, Func<string, A>>, WriteOp<A>>()); /// new Op<A>(f.Left<Func<int, A>, WriteOp<A>>());
}

public static Op<A> ReadLine(Func<string, A> f) {
return new Op<A>(f.Right<Func<int, A>, Func<string, A>>().Left<Either<Func<int, A>, Func<string, A>>, WriteOp<A>>()); /// new Op<A>(f.Left<Func<int, A>, WriteOp<A>>());
}

public static Op<A> WriteOut(char c, A a) {
return new Op<A>(WriteOp<A>.Out(c, a).Right<Either<Func<int, A>, Func<string, A>>, WriteOp<A>>());
}

public static Op<A> WriteErr(char c, A a) {
return new Op<A>(WriteOp<A>.Err(c, a).Right<Either<Func<int, A>, Func<string, A>>, WriteOp<A>>());
}

}

public static class OpExtension {
public static Op<B> Select<A, B>(this Op<A> k, Func<A, B> f) {
return k.Fold(q => Op<B>.Read(f.Compose(q)), q => Op<B>.ReadLine(f.Compose(q)), o => o.Select(f).Op);
}
}

public struct Terminal<A> {
internal readonly Either<A, Op<Terminal<A>>> val;

internal Terminal(Either<A, Op<Terminal<A>>> val) {
this.val = val;
}

public Terminal<C> ZipWith<B, C>(Terminal<B> o, Func<A, Func<B, C>> f) {
return this.SelectMany(a => o.Select(b => f(a)(b)));
}

public Terminal<Pair<A, B>> Zip<B>(Terminal<B> o) {
return ZipWith<B, Pair<A, B>>(o, Pair<A, B>.pairF());
}

public static Terminal<A> Done(A a) {
return new Terminal<A>(a.Left<A, Op<Terminal<A>>>());
}

internal static Terminal<A> More(Op<Terminal<A>> o) {
return new Terminal<A>(o.Right<A, Op<Terminal<A>>>());
}

public static Terminal<Unit> WriteOut(char c) {
return Op<Unit>.WriteOut(c, Unit.Value).Lift;
}

public static Terminal<Unit> WriteErr(char c) {
return Op<Unit>.WriteErr(c, Unit.Value).Lift;
}

public static Terminal<int> Read {
get {
return Op<int>.Read(c => c).Lift;
}
}

public static Terminal<string> ReadLine {
get {
return Op<string>.ReadLine(c => c).Lift;
}
}

// CAUTION: unsafe (perform I/O)
//
// This function stands in for the interpreter of 'Terminal programs'.
public A Run {
get {
return val.Swap.Reduce(o => o.Fold(
r => {
var c = Console.Read();
return r(c).Run;
}
, r => {
var s = Console.ReadLine();
return r(s).Run;
}
, r => {
if(r.IsOut) {
Console.Write(r.Character);
return r.Value.Get.Run;
} else {
Console.Error.Write(r.Character);
return r.Value.Get.Run;
}
}
));
}
}
// Select, SelectMany, Apply, Zip and friends, 8Monad functions
}

public static class TerminalExtension {
public static Terminal<B> Select<A, B>(this Terminal<A> k, Func<A, B> f) {
return k.val.Fold(
a => Terminal<B>.Done(f(a))
, o => Terminal<B>.More(o.Select(t => t.Select(f)))
);
}

public static Terminal<B> SelectMany<A, B>(this Terminal<A> k, Func<A, Terminal<B>> f) {
return k.val.Fold(
f
, o => Terminal<B>.More(o.Select(t => t.SelectMany(f)))
);
}

public static Terminal<C> SelectMany<A, B, C>(this Terminal<A> k, Func<A, Terminal<B>> p, Func<A, B, C> f) {
return SelectMany(k, a => Select(p(a), b => f(a, b)));
}

public static Terminal<B> Apply<A, B>(this Terminal<Func<A, B>> f, Terminal<A> o) {
return f.SelectMany(g => o.Select(p => g(p)));
}

public static Terminal<A> Flatten<A>(this Terminal<Terminal<A>> o) {
return o.SelectMany(z => z);
}

public static Terminal<A> TerminalValue<A>(this A a) {
return Terminal<A>.Done(a);
}
}
}
@@ -88,6 +88,13 @@ public struct Tree<A> {
);
}

public Terminal<Tree<B>> TraverseTerminal<B>(Func<A, Terminal<B>> f) {
return f(root).ZipWith<List<Tree<B>>, Tree<B>>(
children.TraverseTerminal(w => w.TraverseTerminal(f))
, b => bs => b.TreeNode(bs)
);
}

public Input<Tree<B>> TraverseInput<B>(Func<A, Input<B>> f) {
return f(root).ZipWith<List<Tree<B>>, Tree<B>>(
children.TraverseInput(w => w.TraverseInput(f))

0 comments on commit a2bc21e

Please sign in to comment.