IO —
definitely the case. Therefore, under the cut there is a story about how to write an IO
monad in C #, without the slightest attempt to explain why .IO
IO.
IO
considered as a way to deceive the Haskell compiler , to shove the calls of “dirty” native functions into clean code due to the ability to treat them formally as clean because of the addition of fictitious elements to the signature.Get
from the Data.Binary
module) there are functions of type ma -> a
(for Get
this is the runGet
function), that is, it is possible to pull the value out of the monad. For IO
, there is no such function, and the only way to do it is to return the native runtime from the main
function. That is, IO
is a list of actions (script), and the task of clean code is to create this list of actions, but not to perform them.IO
to C # : in the first case we notice that in C # there is no difficulty in calling the “dirty” code from any place, and in the second that in C # all functions and the whole program are there is a list of actions, and the semicolon is nothing more than the monadic operator >>=
.IO
: while the other monads are syntactic sugar, input-output is an operation that is difficult to accomplish while staying within the framework of a clean code. And if you find a way to write a really clean (without monads, without directly or indirectly calling native functions) I / O code, IO
will simply appear as syntactic sugar for these pure functions, an object of the same kind as the Maybe
monad. Well, Maybe
in C # did not write just lazy.beep,
function that returns the number 7 and displays the message “Beep!” And another function returnBeep
, which simply returns the number 7. What can be said about the purity of these functions?returnBeep
clean, but beep
is not: a pure function is a function that does not produce side effects, and in the latter case there is clearly a side effect.returnBeep
function in the program, it must be calculated, and side effects will also be present during the calculation, at least in the form of heat dissipated by computer. But does this mean that the returnBeep
function returnBeep
ceased to be clean because of this? This question can be answered in different ways and - just as in the case of the question "Do parallel lines intersect?" - any of the answers can be taken as an axiom and build on this consistent theory to create a model for some part of the surrounding world. So we will accept as an axiom that the features of the implementation of the calculator and, in particular, the side effects created by it when calculating the function do not affect the purity of the calculated function .returnBeep,
but also beep,
is clean beep,
since issuing a message does not affect the course of the program execution and therefore it is the same implementation feature of the calculator.RealWorld
(see the already mentioned article IO inside ). Direct work with values of this type in Haskell is impossible, but it is easy to turn a value of type RealWorld
into a value of type Integer,
String
or any other, using the available standard functions. The specific type and method of conversion depends on the encoding of the constant of the Universe and the implementation of I / O functions. getOutText :: AppInstance -> IOIndex -> Maybe String getInText :: AppInstance -> IOIndex -> Maybe String
getOutText
function accepts the application instance and the text number in the user and program dialog and returns the corresponding text output by the computer or Nothing
if the input parameters are incorrect. The result Nothing
returned, for example, if the transferred number corresponds to the text entered by the user, and not to the text displayed by the computer. So, if the specified instance of the program did not display anything, then for any value of the number Nothing.
should be returned Nothing.
The getInText
function takes the same arguments and returns the corresponding text entered by the user or Nothing
.getOutText
function, getOutText
more convenient to use the getOutText
function, which is more limited but practical isOutTextEquals
with the following signature and semantics: isOutTextEquals :: String -> AppInstance -> IOIndex -> Bool isOutTextEquals text inst index = getOutText inst index == Just text
What is your name?
Hi, !
IO
monad. The program blank, created by noname, looks like this: module Main_ where import Control.Monad import Data.Vector (Vector, (!?)) import qualified Data.ByteString.Lazy.UTF8 as U import Data.ByteString.Base64.Lazy worldBase64 :: String worldBase64 = "V29ybGQge2FwcEluc3RhbmNlcyA9IFtbSU9PcGVyYXRpb24gSU9Xcml0ZSAiV2hhdCBpcyB5b3Vy" ++ "IG5hbWU/CiIsSU9PcGVyYXRpb24gSU9SZWFkICJub25hbWUiLElPT3BlcmF0aW9uIElPV3JpdGUg" ++ "IkhpLCBub25hbWUhCiJdLFtJT09wZXJhdGlvbiBJT1dyaXRlICJXaGF0IGlzIHlvdXIgbmFtZT8K" ++ "IixJT09wZXJhdGlvbiBJT1JlYWQgIlwxMDQyXDEwNzJcMTA4OVwxMTAzIixJT09wZXJhdGlvbiBJ" ++ "T1dyaXRlICJIaSwgXDEwNDJcMTA3MlwxMDg5XDExMDMhCiJdLFtJT09wZXJhdGlvbiBJT1dyaXRl" ++ "ICJXaGF0IGlzIHlvdXIgbmFtZT8KIixJT09wZXJhdGlvbiBJT1JlYWQgIlwxMDU0XDEwODNcMTEw" ++ "MyIsSU9PcGVyYXRpb24gSU9Xcml0ZSAiSGksIFwxMDU0XDEwODNcMTEwMyEKIl1dfQo=" type AppInstance = Int type IOIndex = Int data IOAction = IORead | IOWrite deriving (Eq, Show, Read) data IOOperation = IOOperation IOAction String deriving (Show, Read) data World = World { appInstances :: Vector (Vector IOOperation) } deriving (Show, Read) world :: World world = read $ U.toString $ decodeLenient $ U.fromString worldBase64 getInOutText :: IOAction -> AppInstance -> IOIndex -> Maybe String getInOutText action app i = do IOOperation actual_action result <- (!? i) <=< (!? app) $ appInstances world if actual_action == action then return result else Nothing getInText :: AppInstance -> IOIndex -> Maybe String getInText = getInOutText IORead getOutText :: AppInstance -> IOIndex -> Maybe String getOutText = getInOutText IOWrite isOutTextEquals :: String -> AppInstance -> IOIndex -> Bool isOutTextEquals text inst index = getOutText inst index == Just text _main :: AppInstance -> Maybe String _main app = do let question = "What is your name?\n" _ <- if isOutTextEquals question app 0 then return () else Nothing name <- getInText app 1 let greeting = "Hi, " ++ name ++ "!\n" _ <- if isOutTextEquals greeting app 2 then return () else Nothing return $ question ++ name ++ "\n" ++ greeting
Main_
and _main
, and, as you can see, the _main
function has the type AppInstance -> Maybe String
. In the noname implementation, _main
returns the dialog protocol — this is not required by the conditions of the problem, but may be useful for debugging purposes.worldBase64
) because it doesn’t know the real one. Therefore, noname developed a time machine (of the original design, with a rather powerful paradoxes built-in) and contacted our Universe by transmitting the program listing and time machine drawings in exchange for the promise to provide it with the exact constant of its universe (more precisely, its parts). supposedly from here it is more visible, than to it from within. module Main where import System.Environment import Data.Vector ((!?)) import qualified Data.Vector as V hiding ((!?)) import Main_ main :: IO () main = do args <- V.fromList <$> getArgs case _main =<< read <$> (args !? 0) of Just text -> putStr text Nothing -> putStrLn "Error!"
_main
function will be available to us. You will also have to manually transmit the launch number ( AppInstance
) as a command line argument, but this is enough to understand and model the functioning of this program in its real environment.wordBase64
do not need to correct the wordBase64
constant: the program will be run only three times (assuming the first test run), and the author will run it all three times, introducing the very names from the very beginning encoded in the text of the program!X,
in fact, returns Either Exception X
; in particular, the void
functions "return" Either Exception ().
(By the way, in Haskell the situation is similar, and the conditional definition of the standard IO
, taking into account the presence of exceptions, does not look like type IO a = RealWorld -> (RealWorld, a),
but rather like type IO a = RealWorld -> (RealWorld, Either SomeException a).
). static void AssertOutTextEquals(string text, AppInstance inst, int index); static string GetInText(AppInstance inst, int index);
AssertOutTextEquals
function is the same isOutTextEquals,
only instead of True
it returns void
without an expression, and instead of False
throws an exception. Similarly, the GetInText
function either returns a non-zero string or throws an exception.AppInstance
, and also use something more idiomatic instead of void
(for functional code): public sealed class None { public static None _ { get { return null; } } None() { } } public sealed class AppInstance { public None AssertOutTextEquals(string text, int index); publi string GetInText(int index); }
None
instead of void
will avoid unnecessary code duplication and make it easier to write the monad itself.Console.Write(string),
and the second is Console.ReadLine().
In addition to these, there are many more useful input and output functions in the Console
class and, using linq expressions, we can summarize our pure functions so as to support them all at once: public None AssertOutTextEquals(Expression<Action> ioExpression, int index); public TResult GetInText(Expression<Func<TResult>> ioExpression, int index);
public None AssertIO(int index, Expression<Action> ioExpression); public TResult AssertIO(int index, Expression<Func<TResult>> ioExpression);
None
were part of a standard ecosystem, instead of Action
we would have Func<None>
and the first function would disappear as a special case of the second. public sealed class AppInstance { readonly static Lazy<AppInstance> inst = new Lazy<AppInstance>(() => new AppInstance((method, argTypes, args) => typeof(Console).GetMethod(method, BindingFlags.Static | BindingFlags.Public, null, argTypes, null).Invoke(null, args))); public static AppInstance Get() { return inst.Value; } readonly Func<string, Type[], object[], object> consoleDescriptor; internal AppInstance(Func<string, Type[], object[], object> consoleDescriptor) { this.consoleDescriptor = consoleDescriptor; } } public static class AppInstanceTestExtensions { public static AppInstance ForTests(this AppInstance inst, Func<string, Type[], object[], object> consoleDescriptor) { return new AppInstance(consoleDescriptor); } }
[TestFixture] public class Tests { TestConsole console; AppInstance testInst; protected void Setup(string input) { console = new TestConsole(input); testInst = AppInstance.Get().ForTests((method, argTypes, args) => { var call = new object[] { console, console.In, console.Out }.Select(x => new { t = x, m = x.GetType().GetMethod(method, argTypes) }).Where(x => xm != null).First(); return call.m.Invoke(call.t, args); }); } } public class TestConsole { readonly MemoryStream output; StreamWriter writer; readonly MemoryStream input; StreamReader reader; public TestConsole(string input) { this.input = new MemoryStream(Encoding.UTF8.GetBytes(input)); this.reader = new StreamReader(this.input); this.output = new MemoryStream(); this.writer = new StreamWriter(this.output); } public TextWriter Out { get { return writer; } } public TextReader In { get { return reader; } } public string Output { get { if(writer != null) { writer.Close(); writer = null; } return Encoding.UTF8.GetString(output.ToArray()); } } }
[Test] public void WriteChars() { Setup(""); testInst.AssertIO(0, () => Console.Write('A')); testInst.AssertIO(1, () => Console.Write('B')); Assert.AreEqual("AB", console.Output); }
AssertIO,
function is AssertIO,
but also the side effects: the code says ... Write('A') ... Write('B') ...,
and it is expected that the screen will be displayed "AB".AssertIO.
calls AssertIO.
Since AssertIO
is a pure function, it would seem that the result (the absence of exceptions) should not change. But this is not the case: this is a different test, there is another AppInstance in it, and therefore the result may change (although it may not change). In practice, it turns out that in this case nothing is output: [Test] public void WriteCharsInBackOrder() { Setup(""); Assert.Throws<ArgumentOutOfRangeException>(() => testInst.AssertIO(1, () => Console.Write('B'))); Assert.Throws<ArgumentOutOfRangeException>(() => testInst.AssertIO(0, () => Console.Write('A'))); Assert.AreEqual("", console.Output); }
[Test] public void WriteCharTwice() { Setup(""); testInst.AssertIO(0, () => Console.Write('A')); testInst.AssertIO(0, () => Console.Write('A')); Assert.Throws<ArgumentException>(() => testInst.AssertIO(0, () => Console.Write('B'))); Assert.AreEqual("A", console.Output); }
[Test] public void GetWriteError() { Setup(""); console.Out.Close(); Assert.Throws<AggregateException>(() => testInst.AssertIO(0, () => Console.Write('A'))); Assert.Throws<ArgumentException>(() => testInst.AssertIO(0, () => Console.Write('B'))); }
[Test] public void ReadChar() { Setup("123"); Assert.AreEqual((int)'1', testInst.AssertIO(0, () => Console.Read())); Assert.AreEqual((int)'2', testInst.AssertIO(1, () => Console.Read())); Assert.AreEqual((int)'3', testInst.AssertIO(2, () => Console.Read())); Assert.AreEqual(-1, testInst.AssertIO(3, () => Console.Read())); }
consoleDescriptor
: class IOOperation<TResult> { readonly string method; readonly Type[] argTypes; readonly object[] args; public IOOperation(LambdaExpression callExpression) { var methodExpr = (MethodCallExpression)callExpression.Body; this.args = methodExpr.Arguments.Select(x => Expression.Lambda<Func<object>>(Expression.Convert(x, typeof(object))).Compile()()).ToArray(); this.method = methodExpr.Method.Name; this.argTypes = methodExpr.Method.GetParameters().Select(x => x.ParameterType).ToArray(); } public TResult Do(Func<string, Type[], object[], object> consoleDescriptor) { return (TResult)consoleDescriptor(method, argTypes, args); } }
WriteCharTwice
test), it is convenient to block Equals: public static bool operator ==(IOOperation<TResult> a, IOOperation<TResult> b) { bool aIsNull = ReferenceEquals(a, null); bool bIsNull = ReferenceEquals(b, null); return aIsNull && bIsNull || !aIsNull && !bIsNull && string.Equals(a.method, b.method, StringComparison.Ordinal) && a.args.Length == b.args.Length && !a.args.Zip(b.args, Equals).Where(x => !x).Any(); } public override int GetHashCode() { return method.GetHashCode() ^ args.Length; } public static bool operator !=(IOOperation<TResult> a, IOOperation<TResult> b) { return !(a == b); } public override bool Equals(object obj) { return this == obj as IOOperation<TResult>; }
AssertIO
methods to one: public None AssertIO(int index, Expression<Action> ioExpression) { return AssertIO(index, new IOOperation<None>(ioExpression)); } public TResult AssertIO<TResult>(int index, Expression<Func<TResult>> ioExpression) { return AssertIO(index, new IOOperation<TResult>(ioExpression)); } TResult AssertIO<TResult>(int index, IOOperation<TResult> operation);
readonly List<IOOperationWithResult> completedOperations = new List<IOOperationWithResult>(); abstract class IOOperationResult { } sealed class IOOperationResult<TResult> : IOOperationResult { readonly TResult returnValue; readonly Exception exception; public IOOperationResult(Func<TResult> getResult) { try { returnValue = getResult(); exception = null; } catch(Exception e) { returnValue = default(TResult); exception = e; } } public TResult Result { get { if(exception != null) throw new AggregateException(exception); return returnValue; } } } abstract class IOOperationWithResult { } sealed class IOOperationWithResult<TResult> : IOOperationWithResult { public IOOperationWithResult(IOOperation<TResult> operation, IOOperationResult<TResult> result) { Operation = operation; Result = result; } public readonly IOOperation<TResult> Operation; public readonly IOOperationResult<TResult> Result; }
AssertIO
: bool rejectOperations = false; TResult AssertIO<TResult>(int index, IOOperation<TResult> operation) { if(index < 0) throw new ArgumentOutOfRangeException("index"); if(index < completedOperations.Count) { var completedOperation = completedOperations[index] as IOOperationWithResult<TResult>; if(completedOperation == null || completedOperation.Operation != operation) throw new ArgumentException("", "operation"); return completedOperation.Result.Result; } if(rejectOperations) throw new ArgumentOutOfRangeException("index"); if(index == completedOperations.Count) { var completedOperation = new IOOperationWithResult<TResult>(operation, new IOOperationResult<TResult>(() => operation.Do(consoleDescriptor))); completedOperations.Add(completedOperation); return completedOperation.Result.Result; } rejectOperations = true; throw new ArgumentOutOfRangeException("index"); }
rejectOperations,
that will ensure the consistency of the method's behavior during further calls.IO
IO
is easy: public sealed class IO<T> { readonly Func<RealWorld, Tuple<RealWorld, T>> func; internal IO(Func<RealWorld, Tuple<RealWorld, T>> func) { this.func = func; } internal RealWorld Execute(RealWorld index, out T result) { var resultTuple = func(index); result = resultTuple.Item2; return resultTuple.Item1; } } class RealWorld { readonly AppInstance inst; readonly int index; public RealWorld(AppInstance inst, int index) { this.inst = inst; this.index = index; } public Tuple<RealWorld, None> Do(Expression<Action> callExpression) { return Tuple.Create(Yield(), inst.AssertIO(index, callExpression)); } public Tuple<RealWorld, TResult> Do<TResult>(Expression<Func<TResult>> callExpression) { return Tuple.Create(Yield(), inst.AssertIO(index, callExpression)); } public RealWorld Yield() { return new RealWorld(inst, index + 1); } }
(from ... in ... select ...).
To do this, besides our custom methods Return
, you Do
will need to implement the methods Select
and SelectMany
(they should be called that way and have a certain signature - duck typing works): public static class IO { public static IO<T> Return<T>(T value) { return new IO<T>(x => Tuple.Create(x, value)); } public static IO<R> Select<T, R>(this IO<T> io, Func<T, R> selector) { return new IO<R>(x => { T t; var index = io.Execute(x, out t); return Tuple.Create(index, selector(t)); }); } public static IO<R> SelectMany<T, C, R>(this IO<T> io, Func<T, IO<C>> selector, Func<T, C, R> projector) { return new IO<R>(x => { T t; var index = io.Execute(x, out t); var ioc = selector(t); C c; var resultIndex = ioc.Execute(index, out c); return Tuple.Create(resultIndex, projector(t, c)); }); } public static IO<None> Do(Expression<Action> callExpression) { return new IO<None>(x => x.Do(callExpression)); } public static IO<TResult> Do<TResult>(Expression<Func<TResult>> callExpression) { return new IO<TResult>(x => x.Do(callExpression)); } public static IO<T> Handle<T>(this IO<T> io, Func<Exception, IO<T>> handler) { return new IO<T>(x => { RealWorld rw; T t; try { rw = io.Execute(x, out t); } catch(Exception e) { rw = handler(e).Execute(x.Yield(), out t); } return Tuple.Create(rw, t); }); } }
Handle
that allows you to continue working when an event occurs. public static class AppInstanceIOExtensions { public static void DoMain(this AppInstance inst, Func<IO<None>> body) { None result; body().Execute(new RealWorld(inst, 0), out result); } }
IO
: class Program { static void Main(string[] args) { AppInstance.Get().DoMain(IOMain); } static IO<None> IOMain() { return from _ in IO.Do(() => Console.WriteLine("What is your name?")) from name in IO.Do(() => Console.ReadLine()) let message = "Hi, " + name + "!" from r in IO.Do(() => Console.WriteLine(message)) select r; } }
Source: https://habr.com/ru/post/282940/
All Articles