on master help sign in
Fix code block visibility.
commitddff903c81811e50f3465496f42c90e41e9e2a3e authorSigilante <57601680+sigilante@users.noreply.github.com> dateThu 14 Jul 2022, 22:47 (4 years ago) parentaca33b7
1 file changed+145−133
content/blog/io-in-hoon.md+145−133
content/blog/io-in-hoon.md+145−133
@@ -25,7 +25,9 @@ Imperative languages (such as C and Python) perform IO by calling a function in
2525
2626In this example and others to follow, the task will be to read a filename from the command line, read a URL out of that file, and fetch the contents at that URL.
2727
28− output = fetchUrl(stripWhitespace(readFile(cliInput())))
28+```
29+output = fetchUrl(stripWhitespace(readFile(cliInput())))
30+```
2931
3032For readability, intermediate variables would be a good idea, but nothing stops you from nesting IO this way.
3133
@@ -33,9 +35,11 @@ For readability, intermediate variables would be a good idea, but nothing stops
3335
3436Languages that use explicit monads for IO (such as Haskell) perform IO by running IO behind a bind operation, which I'll notate with `<-`. Thus, while you can't put IO _anywhere_, the general structure is a sequence of IO operations.
3537
36− fileName <- cliInput()
37− url <- readFile(fileName)
38− output <- fetchUrl(stripWhitespace(url))
38+```
39+fileName <- cliInput()
40+url <- readFile(fileName)
41+output <- fetchUrl(stripWhitespace(url))
42+```
3943
4044[Tackling the Awkward Squad](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/07/mark.pdf) is the canonical description of monadic IO, and does a great job of explaining how the Haskell community decided to use this as their primary IO style.
4145
@@ -43,22 +47,24 @@ Languages that use explicit monads for IO (such as Haskell) perform IO by runnin
4347
4448Languages that use explicit state machines for IO (such as Elm) perform IO by accepting events and producing a list of effects.
4549
46− event :: Initialize
47− | CLIInput text
48− | FileInput text
49− | HTTPResponse body
50− effect :: CLIInput
51− | ReadFile fileName
52− | FetchUrl url
53− | Print text
54− main(event) {
55− switch event {
56− Initialize -> [CLIInput ~]
57− CLIinput fileName -> [ReadFile fileName]
58− FileInput url -> [FetchUrl stripWhitespace(url)]
59− HTTPResponse body -> [Print body]
60− }
61− }
50+```
51+event :: Initialize
52+ | CLIInput text
53+ | FileInput text
54+ | HTTPResponse body
55+effect :: CLIInput
56+ | ReadFile fileName
57+ | FetchUrl url
58+ | Print text
59+main(event) {
60+ switch event {
61+ Initialize -> [CLIInput ~]
62+ CLIinput fileName -> [ReadFile fileName]
63+ FileInput url -> [FetchUrl stripWhitespace(url)]
64+ HTTPResponse body -> [Print body]
65+ }
66+}
67+```
6268
6369The effects produced are not functions but data.
6470
@@ -70,39 +76,45 @@ Let's look at another type of program: a simple HTTP server that you can give te
7076
7177Imperative:
7278
73− text = "no text entered yet"
74− while (true)
75− cli = readCli()
76− if (cli != Null)
77− text = cli
78− request = readHttp()
79− if (request != Null)
80− respond(request,text)
79+```
80+text = "no text entered yet"
81+while (true)
82+ cli = readCli()
83+ if (cli != Null)
84+ text = cli
85+ request = readHttp()
86+ if (request != Null)
87+ respond(request,text)
88+```
8189
8290Monadic:
8391
84− loop("no text entered yet")
85−
86− where:
87−
88− loop :: text -> IO Null
89− loop =
90− input <- readInput()
91− switch input
92− CLIInput newText -> loop(newText)
93− HTTPRequest request ->
94− _ <- respond(request,text)
95− loop(text)
92+```
93+loop("no text entered yet")
94+
95+where:
96+
97+loop :: text -> IO Null
98+loop =
99+ input <- readInput()
100+ switch input
101+ CLIInput newText -> loop(newText)
102+ HTTPRequest request ->
103+ _ <- respond(request,text)
104+ loop(text)
105+```
96106
97107State machine:
98108
99− text = "no text entered yet"
100− main(text,event) {
101− switch event {
102− CLIInput newText -> [newText, Null]
103− HTTPRequest request -> [text, HTTPResponse text]
104− }
105− }
109+```
110+text = "no text entered yet"
111+main(text,event) {
112+ switch event {
113+ CLIInput newText -> [newText, Null]
114+ HTTPRequest request -> [text, HTTPResponse text]
115+ }
116+}
117+```
106118
107119Again, note the response in the state machine is data, not a function.
108120
@@ -155,16 +167,16 @@ However, Arvo is a single-level store — all of its state is permanent. This is
155167Further, Arvo and the vanes rarely perform long sequences of IO. Generally, they do one thing and emit some effects, similar to the HTTP server example above. Sometimes they "pass through" a request. For example, the Eyre vane passes HTTP requests to userspace agents or threads. In this case, there is a sequence of IO, which in monadic code would be:
156168
157169```
158− loop()
170+loop()
159171
160− where:
172+where:
161173
162− loop =
163− request <- receiveRequest()
164− agent = lookupAgent(request)
165− response <- callAgent(agent, request)
166− _ <- sendResponse(request, response)
167− loop()
174+loop =
175+ request <- receiveRequest()
176+ agent = lookupAgent(request)
177+ response <- callAgent(agent, request)
178+ _ <- sendResponse(request, response)
179+ loop()
168180```
169181
170182This is clear code to read, but it leaves open the question of how to respond during the callAgent() function. If another request comes in, can we handle it in parallel? If we need to upgrade ourselves before the agent responds, can we make sure to properly handle the response?
@@ -174,16 +186,16 @@ The naive solution in monadic code is to say that you can only handle one reques
174186In a state machine, the naive solution is:
175187
176188```
177− state :: (map request agent)
178− main(state, event) {
179− switch event {
180− HTTPRequest request ->
181− agent = lookupAgent(request)
182− [put(state, request, agent), CallAgent agent request]
183− AgentResponse request response ->
184− [del(state, request), HTTPRespond request response]
185− }
186− }
189+state :: (map request agent)
190+main(state, event) {
191+ switch event {
192+ HTTPRequest request ->
193+ agent = lookupAgent(request)
194+ [put(state, request, agent), CallAgent agent request]
195+ AgentResponse request response ->
196+ [del(state, request), HTTPRespond request response]
197+ }
198+}
187199```
188200
189201Upgrading this is trivial, since the state (map of requests to outstanding calls to agents) is explicit. It also trivially handles concurrent requests.
@@ -201,61 +213,61 @@ However, userspace sometimes needs to perform long sequences of IO. Let's look a
201213Monadic:
202214
203215```
204− topStories <- fetch(topStoriesUrl)
205− loop(topStories)
206−
207− where:
208−
209− loop :: topStories -> IO Null
210− if topStories is Null
211− return Null
212− comments <- retryLoop(5,head(topStories))
213− otherComments <- loop(tail(topStories))
214− append(comments,otherComments)
215−
216− retryLoop :: [n story] -> IO Comments
217− comments <- fetchComments(story)
218− if comments is HTTPError:
219− if n == 0:
220− bail
221− else:
222− retryLoop(n-1, story)
216+topStories <- fetch(topStoriesUrl)
217+loop(topStories)
218+
219+where:
220+
221+loop :: topStories -> IO Null
222+ if topStories is Null
223+ return Null
224+ comments <- retryLoop(5,head(topStories))
225+ otherComments <- loop(tail(topStories))
226+ append(comments,otherComments)
227+
228+retryLoop :: [n story] -> IO Comments
229+ comments <- fetchComments(story)
230+ if comments is HTTPError:
231+ if n == 0:
232+ bail
233+ else:
234+ retryLoop(n-1, story)
223235```
224236
225237State machine:
226238
227239```
228− state :: Initial
229− | FetchingTop
230− | FetchingStory comments stories retries
231− | Done comments
232−
233− main(state, event) {
234− switch event {
235− Initialize ->
236− assert state == Initial
237− [FetchingTop, Fetch topStoriesUrl]
238− HttpResponse response ->
239− switch state {
240− FetchingTop ->
241− [FetchingStory Null stories 5, FetchComments head(stories)]
242− FetchingStory comments stories retries ->
243− if response is HTTPError:
244− if retries == 0:
245− [Initial, Null]
246− else:
247− [FetchingStory comments stories retries-1,
248− FetchComments head(stories)]
249− else:
250− comments = append(response,comments)
251− if tail(stories) is Null:
252− [Done comments, Null]
253− else:
254− [FetchingStory append(comments,response) tail(stories) 5,
255− FetchComments head(tail(stories))]
256− }
240+state :: Initial
241+ | FetchingTop
242+ | FetchingStory comments stories retries
243+ | Done comments
244+
245+main(state, event) {
246+ switch event {
247+ Initialize ->
248+ assert state == Initial
249+ [FetchingTop, Fetch topStoriesUrl]
250+ HttpResponse response ->
251+ switch state {
252+ FetchingTop ->
253+ [FetchingStory Null stories 5, FetchComments head(stories)]
254+ FetchingStory comments stories retries ->
255+ if response is HTTPError:
256+ if retries == 0:
257+ [Initial, Null]
258+ else:
259+ [FetchingStory comments stories retries-1,
260+ FetchComments head(stories)]
261+ else:
262+ comments = append(response,comments)
263+ if tail(stories) is Null:
264+ [Done comments, Null]
265+ else:
266+ [FetchingStory append(comments,response) tail(stories) 5,
267+ FetchComments head(tail(stories))]
257268 }
258− }
269+ }
270+}
259271```
260272
261273With practice, the state machine version of this can be written correctly. However, (1) it's verbose, (2) the control flow jumps all over the place, and (3) in practice it's challenging to get exactly right. This is still a small example, and it gets much worse as the length and complexity of the IO sequence grows.
@@ -285,7 +297,7 @@ We're in a bit of a bind, though: Jael has to be a state machine. The solution i
285297This thread has a definite purpose: given the most recent block number we already know about, fetch all the PKI transactions since then. The function signature is:
286298
287299```
288− syncEthereum oldBlockNumber -> IO [newBlockNumber, newTxs]
300+syncEthereum oldBlockNumber -> IO [newBlockNumber, newTxs]
289301```
290302
291303Jael runs this thread every five minutes and, if it succeeds, then we update our block number and PKI state. If the thread fails, gets stuck, or we need to upgrade it, we just kill the old thread and start a new one. Again, a few extra HTTP requests don’t matter.
@@ -293,24 +305,24 @@ Jael runs this thread every five minutes and, if it succeeds, then we update our
293305From Jael's perspective, it looks something like:
294306
295307```
296− state :: State block txs outstandingThread
297− main(state,event) {
298− switch event {
299− Initialize -> [State 0 Null Null, StartTimer (now + five minutes)]
300− TimerFired ->
301− effects = If outstandingThread is Null
302− then Null
303− else [Kill outstandingThread]
304− newId = newThreadId()
305− [State txs newId,
306− append(effects,
307− StartThread newId syncEthereum(block),
308− StartTimer (now + five minutes))]
309− ThreadFinished Failure -> [State block txs Null, Null]
310− ThreadFinished Success newBlockNumber newTxs ->
311− [State newBlockNumber append(txs,newTxs) Null, Null]
312− }
313− }
308+state :: State block txs outstandingThread
309+main(state,event) {
310+ switch event {
311+ Initialize -> [State 0 Null Null, StartTimer (now + five minutes)]
312+ TimerFired ->
313+ effects = If outstandingThread is Null
314+ then Null
315+ else [Kill outstandingThread]
316+ newId = newThreadId()
317+ [State txs newId,
318+ append(effects,
319+ StartThread newId syncEthereum(block),
320+ StartTimer (now + five minutes))]
321+ ThreadFinished Failure -> [State block txs Null, Null]
322+ ThreadFinished Success newBlockNumber newTxs ->
323+ [State newBlockNumber append(txs,newTxs) Null, Null]
324+ }
325+}
314326```
315327
316328In other words, from Jael's perspective, it's just a single IO event that encapsulates arbitrarily complex IO in the thread. A single IO event is easy enough, and we can maintain all our permanency and robustness guarantees, because we know the thread will have one of three results: success, failure, or it hangs. If we set a timeout, hang becomes failure, so there's only two possible results: success or failure. If we handle both of those correctly, we've handled all the possible IO errors that could have occurred.