Skip to content

Commit 25b40b6

Browse files
authored
Refactor IO actions (#20)
1 parent b34be63 commit 25b40b6

File tree

6 files changed

+88
-91
lines changed

6 files changed

+88
-91
lines changed

examples/hello2.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
from oslash import Put, Get, IO
1+
from oslash import Put, Get, Return, Unit
22

33
main = Put("What is your name?",
44
Get(lambda name:
55
Put("What is your age?",
66
Get(lambda age:
77
Put("Hello " + name + "!",
88
Put("You are " + age + " years old",
9-
IO()
9+
Return(Unit)
1010
)
1111
)
1212
)

oslash/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
from .maybe import Maybe, Just, Nothing
55
from .either import Either, Right, Left
66
from .list import List
7-
from .ioaction import IO, Put, Get, ReadFile, put_line, get_line, read_file
7+
from .ioaction import IO, Put, Get, Return, ReadFile, put_line, get_line, read_file
88
from .writer import Writer, MonadWriter, StringWriter
99
from .reader import Reader, MonadReader
1010
from .identity import Identity
1111
from .state import State
1212
from .do import do, let, guard
1313

1414
from .monadic import *
15-
from .util import fn
15+
from .util import fn, Unit
1616

1717
from ._version import get_versions
1818
__version__ = get_versions()['version']

oslash/cont.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
TCont = Callable[[T], TResult]
2020

21+
2122
class Cont(Generic[T, TResult]):
2223
"""The Continuation Monad.
2324
@@ -39,7 +40,7 @@ def unit(cls, value: T) -> 'Cont[T, TResult]':
3940
4041
Haskell: a -> Cont a
4142
"""
42-
fn : Callable[[TCont], TResult] = lambda cont: cont(value)
43+
fn: Callable[[TCont], TResult] = lambda cont: cont(value)
4344
return Cont(fn)
4445

4546
def map(self, fn: Callable[[T], T2]) -> 'Cont[T2, TResult]':

oslash/ioaction.py

Lines changed: 75 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
"""Implementation of IO Actions.
1+
"""Implementation of IO Actions."""
22

3-
Many thanks to Chris Taylor and his excellent blog post "IO Is Pure",
4-
http://chris-taylor.github.io/blog/2013/02/09/io-is-not-a-side-effect/
5-
"""
6-
7-
from typing import Any, Callable, Generic, TypeVar, Tuple, Optional
3+
from abc import abstractmethod
4+
from typing import Any, Callable, Generic, TypeVar, Tuple
85

96
from .typing import Applicative
107
from .typing import Functor
118
from .typing import Monad
12-
from .util import indent as ind
9+
from .util import indent as ind, Unit
1310

1411
TSource = TypeVar("TSource")
1512
TResult = TypeVar("TResult")
@@ -24,40 +21,24 @@ class IO(Generic[TSource]):
2421
happen.
2522
"""
2623

27-
def __init__(self, value: Optional[TSource] = None) -> None:
28-
"""Create IO Action."""
29-
30-
super().__init__()
31-
self._value = value
32-
3324
@classmethod
3425
def unit(cls, value: TSource):
35-
return cls(value)
26+
return Return(value)
3627

37-
def bind(self, func: Callable[[Optional[TSource]], "IO[Optional[TResult]]"]) -> "IO[Optional[TResult]]":
28+
@abstractmethod
29+
def bind(self, func: Callable[[TSource], "IO[TResult]"]) -> "IO[TResult]":
3830
"""IO a -> (a -> IO b) -> IO b."""
3931

40-
return func(self._value)
41-
42-
@classmethod
43-
def pure(
44-
cls, value: Optional[Callable[[Optional[TSource]], Optional[TResult]]]
45-
) -> "IO[Optional[Callable[[Optional[TSource]], Optional[TResult]]]]":
46-
return IO(value)
47-
48-
def apply(
49-
self: "IO[Optional[Callable[[Optional[TSource]], Optional[TResult]]]]", something: "IO[Optional[TSource]]"
50-
) -> "IO[Optional[TResult]]":
51-
"""Apply wrapped function over something."""
52-
assert self._value is not None
53-
return something.map(self._value)
32+
raise NotImplementedError
5433

55-
def map(self, func: Callable[[Optional[TSource]], Optional[TResult]]) -> "IO[Optional[TResult]]":
56-
return IO(func(self._value))
34+
@abstractmethod
35+
def map(self, func: Callable[[TSource], TResult]) -> "IO[TResult]":
36+
raise NotImplementedError
5737

58-
def run(self, world: int) -> Optional[TSource]:
38+
@abstractmethod
39+
def run(self, world: int) -> TSource:
5940
"""Run IO action."""
60-
return self._value
41+
raise NotImplementedError
6142

6243
def __or__(self, func):
6344
"""Use | as operator for bind.
@@ -67,49 +48,71 @@ def __or__(self, func):
6748
return self.bind(func)
6849

6950
def __call__(self, world: int = 0) -> Any:
70-
"""Nothing more to run."""
51+
"""Run io action."""
7152
return self.run(world)
7253

54+
@abstractmethod
7355
def __str__(self, m: int = 0, n: int = 0) -> str:
74-
a = self._value
75-
return "%sReturn %s" % (ind(m), [a])
56+
raise NotImplementedError
7657

7758
def __repr__(self) -> str:
7859
return self.__str__()
7960

8061

81-
class Put(IO):
62+
class Return(IO[TSource]):
63+
def __init__(self, value: TSource) -> None:
64+
"""Create IO Action."""
65+
66+
self._value = value
67+
68+
def map(self, func: Callable[[TSource], TResult]) -> "IO[TResult]":
69+
return Return(func(self._value))
70+
71+
def bind(self, func: Callable[[TSource], "IO[TResult]"]) -> "IO[TResult]":
72+
"""IO a -> (a -> IO b) -> IO b."""
73+
74+
return func(self._value)
75+
76+
def run(self, world: int) -> TSource:
77+
"""Run IO action."""
78+
return self._value
79+
80+
def __str__(self, m: int = 0, n: int = 0) -> str:
81+
a = self._value
82+
return f"{ind(m)}Return {a}"
83+
84+
85+
class Put(IO[TSource]):
8286
"""The Put action.
8387
8488
A container holding a string to be printed to stdout, followed by
8589
another IO Action.
8690
"""
8791

88-
def __init__(self, text: str, action: IO) -> None:
89-
super().__init__((text, action))
92+
def __init__(self, text: str, io: IO) -> None:
93+
self._value = text, io
9094

91-
def bind(self, func: Callable[[Optional[TSource]], IO[Optional[TResult]]]) -> "Put":
95+
def bind(self, func: Callable[[TSource], IO[TResult]]) -> 'IO[TResult]':
9296
"""IO a -> (a -> IO b) -> IO b"""
9397

94-
assert self._value is not None
95-
text, a = self._value
96-
return Put(text, a.bind(func))
98+
text, io = self._value
99+
return Put(text, io.bind(func))
97100

98-
def map(self, func: Callable[[Optional[TSource]], Optional[TResult]]) -> "Put":
101+
def map(self, func: Callable[[TSource], TResult]) -> "IO[TResult]":
99102
# Put s (fmap f io)
100103
assert self._value is not None
101104
text, action = self._value
102105
return Put(text, action.map(func))
103106

104-
def run(self, world: int) -> IO:
107+
def run(self, world: int) -> TSource:
105108
"""Run IO action"""
106109

107110
assert self._value is not None
108111
text, action = self._value
109112
new_world = pure_print(world, text)
110113
return action(world=new_world)
111114

112-
def __call__(self, world: int = 0) -> IO:
115+
def __call__(self, world: int = 0) -> TSource:
113116
return self.run(world)
114117

115118
def __str__(self, m: int = 0, n: int = 0) -> str:
@@ -119,96 +122,91 @@ def __str__(self, m: int = 0, n: int = 0) -> str:
119122
return '%sPut ("%s",\n%s\n%s)' % (ind(m), s, a, ind(m))
120123

121124

122-
class Get(IO):
123-
"""A container holding a function from string -> IO, which can
125+
class Get(IO[TSource]):
126+
"""A container holding a function from string -> IO[TSource], which can
124127
be applied to whatever string is read from stdin.
125128
"""
126129

127-
def __init__(self, func: Callable[[str], IO]) -> None:
128-
super().__init__(func)
130+
def __init__(self, fn: Callable[[str], IO[TSource]]) -> None:
131+
self._fn = fn
129132

130-
def bind(self, func: Callable[[Any], IO]) -> IO:
133+
def bind(self, func: Callable[[TSource], IO[TResult]]) -> IO[TResult]:
131134
"""IO a -> (a -> IO b) -> IO b"""
132135

133-
assert self._value is not None
134-
g = self._value
136+
g = self._fn
135137
return Get(lambda text: g(text).bind(func))
136138

137-
def map(self, func: Callable[[Any], Any]) -> "Get":
139+
def map(self, func: Callable[[TSource], TResult]) -> IO[TResult]:
138140
# Get (\s -> fmap f (g s))
139-
assert self._value is not None
140-
g = self._value
141+
g = self._fn
141142
return Get(lambda s: g(s).map(func))
142143

143-
def run(self, world: int) -> IO:
144+
def run(self, world: int) -> TSource:
144145
"""Run IO Action"""
145146

146-
assert self._value is not None
147-
func = self._value
147+
func = self._fn
148148
new_world, text = pure_input(world)
149149
action = func(text)
150150
return action(world=new_world)
151151

152-
def __call__(self, world: int = 0) -> IO:
152+
def __call__(self, world: int = 0) -> TSource:
153153
return self.run(world)
154154

155155
def __str__(self, m: int = 0, n: int = 0) -> str:
156-
assert self._value is not None
157-
g = self._value
156+
g = self._fn
158157
i = "x%s" % n
159158
a = g(i).__str__(m + 1, n + 1)
160159
return "%sGet (%s => \n%s\n%s)" % (ind(m), i, a, ind(m))
161160

162161

163-
class ReadFile(IO):
164-
"""A container holding a filename and a function from string -> IO,
162+
class ReadFile(IO[str]):
163+
"""A container holding a filename and a function from string -> IO[str],
165164
which can be applied to whatever string is read from the file.
166165
"""
167166

168167
def __init__(self, filename: str, func: Callable[[str], IO]) -> None:
169-
super().__init__((filename, func))
170168
self.open_func = open
171-
self._get_value = lambda: (filename, func)
169+
self._value = filename, func
172170

173171
def bind(self, func: Callable[[Any], IO]) -> IO:
174172
"""IO a -> (a -> IO b) -> IO b"""
175173

176-
filename, g = self._get_value()
174+
filename, g = self._value
177175
return ReadFile(filename, lambda s: g(s).bind(func))
178176

179177
def map(self, func: Callable[[Any], Any]) -> IO:
180178
# Get (\s -> fmap f (g s))
181-
filename, g = self._get_value()
179+
filename, g = self._value
182180
return Get(lambda s: g(s).map(func))
183181

184-
def run(self, world: int) -> IO:
182+
def run(self, world: int) -> str:
185183
"""Run IO Action"""
186184

187-
filename, func = self._get_value()
185+
filename, func = self._value
188186
f = self.open_func(filename)
189187
action = func(f.read())
190188
return action(world=world + 1)
191189

192-
def __call__(self, world: int = 0) -> IO:
190+
def __call__(self, world: int = 0) -> str:
193191
return self.run(world)
194192

195193
def __str__(self, m: int = 0, n: int = 0) -> str:
196-
filename, g = self._get_value()
194+
filename, g = self._value
197195
i = "x%s" % n
198196
a = g(i).__str__(m + 2, n + 1)
199197
return '%sReadFile ("%s",%s => \n%s\n%s)' % (ind(m), filename, i, a, ind(m))
200198

201199

202-
def get_line() -> IO:
203-
return Get(lambda text: IO(text))
200+
def get_line() -> IO[str]:
201+
return Get(Return)
204202

205203

206-
def put_line(string: str) -> IO:
207-
return Put(string, IO(None))
204+
def put_line(text: str) -> IO:
205+
return Put(text, Return(Unit))
208206

209207

210208
def read_file(filename: str) -> IO:
211-
return ReadFile(filename, lambda text: IO(text))
209+
return ReadFile(filename, Return)
212210

213211

214212
def pure_print(world: int, text: str) -> int:
@@ -222,17 +220,13 @@ def pure_input(world: int) -> Tuple[int, str]:
222220

223221

224222
assert isinstance(IO, Functor)
225-
assert isinstance(IO, Applicative)
226223
assert isinstance(IO, Monad)
227224

228225
assert isinstance(Put, Functor)
229-
assert isinstance(Put, Applicative)
230226
assert isinstance(Put, Monad)
231227

232228
assert isinstance(Get, Functor)
233-
assert isinstance(Get, Applicative)
234229
assert isinstance(Get, Monad)
235230

236231
assert isinstance(ReadFile, Functor)
237-
assert isinstance(ReadFile, Applicative)
238232
assert isinstance(ReadFile, Monad)

oslash/list.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from functools import partial, reduce
22

3-
from typing import Generic, Callable, Iterator, TypeVar, Iterable, Sized, Union, Any, cast, Optional
3+
from typing import Callable, Iterator, TypeVar, Iterable, Sized, Any, cast, Optional
44

55
from .typing import Applicative
66
from .typing import Functor
@@ -11,13 +11,14 @@
1111
TResult = TypeVar("TResult")
1212
TSelector = Callable[[TSource, Optional[Callable]], Any]
1313

14-
class List(Iterable[TSource]):
14+
15+
class List(Iterable[TSource], Sized):
1516
"""The list monad.
1617
1718
Wraps an immutable list built from lambda expressions.
1819
"""
1920

20-
def __init__(self, lambda_list: Optional[Callable[[TSelector], Any]]=None) -> None:
21+
def __init__(self, lambda_list: Optional[Callable[[TSelector], Any]] = None) -> None:
2122
"""Initialize List."""
2223

2324
self._value = lambda_list

tests/test_ioaction.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from typing import Tuple
33

44
import oslash.ioaction
5-
from oslash import Put, IO, put_line, get_line
5+
from oslash import Put, Return, put_line, get_line
6+
from oslash.util import Unit
67

78

89
class MyMock:
@@ -33,6 +34,6 @@ def test_put_line(self) -> None:
3334

3435
def test_put_return(self) -> None:
3536
pm = MyMock()
36-
action = Put("hello, world!", IO())
37+
action = Put("hello, world!", Return(Unit))
3738
action()
3839
self.assertEqual(pm.value, "hello, world!")

0 commit comments

Comments
 (0)