-
Notifications
You must be signed in to change notification settings - Fork 6
/
day11.html
1456 lines (1208 loc) · 82.9 KB
/
day11.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Cats Effect & Http4s</title>
<link rel="stylesheet" href="css/reveal.css">
<link rel="stylesheet" href="css/theme/moon.css">
<!-- Theme used for syntax highlighting of code -->
<link rel="stylesheet" href="lib/css/zenburn.css">
<!-- Printing and PDF exports -->
<script>
var link = document.createElement( 'link' );
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = window.location.search.match( /print-pdf/gi ) ? 'css/print/pdf.css' : 'css/print/paper.css';
document.getElementsByTagName( 'head' )[0].appendChild( link );
</script>
</head>
<body>
<div class="reveal">
<div class="slides">
<section>
<h3>Часть 11. Cats Effect & Http4s</h3>
<p><small>Страничка курса: <a href="https://maxcom.github.io/scala-course-2022/">https://maxcom.github.io/scala-course-2022/</a></small></p>
<aside class="notes">
Добрый день.<p>
Это 11 лекция курса по программированию на языке Scala.<p>
На этой лекции мы проведём обзор двух библиотек из экосистемы Typelevel: cast-effect и http4s.
</aside>
</section>
<section>
<h3>План</h3>
<ol>
<li>Эффекты</li>
<li>IO vs Future</li>
<li>Trampolining</li>
<li>IO API</li>
<li>Resource</li>
<li>Thread Model: Fibers</li>
<li>Http4s</li>
</ol>
<aside class="notes">
В начале мы познакомимся с понятием эффектов в программировании.<p>
Далее рассмотрим монаду IO, которая предлагается в библиотеке cats-effect как альтернатива для стандартного Future, и сравним их между собой.<p>
Особое внимание уделим приёму trampolining, который используется в этой монаде.<p>
Далее рассмотрим как cats effect предлагает работать с ресурсами, чтобы максимально сохранять безопасность кода.<p>
Обязательно разберём предлагаемую модель потоков и понятие файберов.<p>
В конце лекции сделаем быстрый обзор на минималистичный сервер http4s, который очень прост в использовании и отлично сочетается с cats-effect.
</aside>
</section>
<section>
<h3>Чистая функция</h3>
<ul>
<li>Является детерминированной</li>
<li>Не обладает побочными эффектами (side-effect)</li>
</ul>
<aside class="notes">
Считается, что в идеальном функциональном мире любая функция должна быть чистой
(т.е. детерминированной и не обладать побочными эффектами).<p>
Напомню, что детерминированность означает, что сколько бы раз вы ни вызывали функцию,
на одних и тех же входных данных она будет давать всегда один и тот же ответ.<p>
Теперь давайте вспомним, что такое побочные эффекты.
</aside>
</section>
<section>
<h3>Побочный эффект</h3>
<ul>
<li>Создание или изменение файла</li>
<li>Запись данных в базу</li>
<li>Изменение глобальной переменной (увеличение счётчика)</li>
<li class=fragment>Модификация переданной в функцию переменной</li>
<li class=fragment>Изменение своего дальнейшего поведения</li>
<li class=fragment>Вызов внешней функции, имеющей любой из перечисленных выше эффектов</li>
</ul>
<aside class="notes">
Представим, что мы хотим, чтобы в результате каких-то вычислений,
в результате получения новой информации или команды
программа что-то записала в базу данных, в файл или изменила значение счётчика.<p>
Всё это - изменения каких-то ВНЕШНИХ по отношению к программе "ресурсов".<hr>
В частности к изменению таких внешних ресурсов относится и модификация переданной в функцию переменной.<hr>
Также к побочным эффектам относится изменение своего ВНУТРЕННЕГО состояния таким образом,
что в дальнейшем внешние проявления в поведении программы будут отличаться от предыдущих.<hr>
И, наконец, вызов какой-то внешней функции, приводящий к любому из этих последствий,
также будет нарушать чистоту нашей функции и считаться side-effect-ом.
</aside>
</section>
<section>
<p>Чистые функции - это хорошо?</p>
<aside class="notes">
Так почему же чистые функции - это хорошо?<p>
Как правило, наличие side-effect-а влечёт за собой нарушение детерминированности.<p>
Но бывает и что-то одно: только детерминированные функции без side-эффектов.
Например, функция random или функция получения значения из какой-то глобальной переменной. Или функция чтения из базы данных или файла.<p>
И наоборот, функции с side-эффектами, но при этом детерминированные. Например, функция print, которая всегда возвращает 0.
</aside>
</section>
<section>
<h3>Чистые функции</h3>
<ul>
<li>Делают код более предсказуемым</li>
<li>Решают проблемы многопоточности</li>
<li>Позволяют кешировать результат</li>
<li>Можно менять местами последовательность вызова двух чистых функций</li>
</ul>
<aside class="notes">
В любом случае функции, не являющиеся чистыми, делают поведение программы труднопредсказуемым и не однозначным.<p>
Начинают вылезать странности при одновременном вызове одной и той же функции в нескольких потоках.
Например, когда несколько пользователей запрашивают что-то у сервиса, то могут получить различные результаты в зависимости от порядка выполнения запросов.<p>
Реализовывать асинхронные программы становится сложнее, потому что результаты одного потока могут зависить от выполнения другого потока.<p>
Все эти проблемы решаются чистыми функциями.<p>
Более того, чистые функции позволяют кешировать результат и заменять вызов функции на значение её результата.
А ещё можно смело менять последовательность вызова таких функций не опасаясь за результат.
<p>
Но что же делать, если почти всегда "полезными результатами" работы программы являются какие-то побочные эффекты?
Ведь вся цель написания программы - это выполнить какие-то действия и что-то поменять в окружающем мире!
</aside>
</section>
<section>
<h3>Resource</h3>
<p>Внешний относительно функции объект,<br>который может меняться со временем</p><p>(не обязательно побочными эффектами<br>данной функции)</p>
<aside class="notes">
Здесь на помощь приходит понятие "ресурса".
Это некоторый "внешний" относительно функции объект, который может менять своё состояние с течением времени.
Например, это может быть тот же файл на диске, база данных, глобальная переменная, консоль ввода-вывода, какая-нибудь стейт-машина.<p>
В итоге всё сводится к тому, чтобы максимально локализовать работу с ресурсами и
обеспечить безопасное использование ресурсов несколькими потоками.
</aside>
</section>
<section>
<p>Методы для работы с ресурсами стоит рассматривать как единичное и неделимое действие,
а в конце этих действий нужно полностью освобождать ресурс</p>
<aside class="notes">
Методы для работы с ресурсами лучше всего рассматривать как единичное и неделимое действие,
а в конце этих действий нужно полностью освобождать ресурс.<p>
Вся остальная программа при этом состоит из чистых функций, из которых разработчик проектирует как из кирпичиков,
и обращение к ресурсам становится максимально похоже на эти кирпичики.<p>
При таком подходе работа с ресурсами становится гораздо более предсказуемой и максимально близкой к чистым функциям,
с тем лишь исключением, что остаётся некоторый "внешний эффект":
Например, записанные в этот ресурс данные или изменённое состояние ресурса.<p>
Заметим, что, текущее состояния объекта можно рассматривать, как некоторую информацию, которую этот объект содержит.
Поэтому нет никого смысла рассматривать этот случай отдельно.
</aside>
</section>
<section>
<div><img src="cats-effect-logo.svg" height="200" style="background: none; border: 0; box-shadow: none;"></div>
<h3>Cats Effect</h3>
<small><a href="https://typelevel.org/cats-effect">https://typelevel.org/cats-effect</a></small>
<aside class="notes">
С тем, что такое эффекты и как с ними лучше работать мы разобрались.
Теперь давайте посмотрим, что нам предлагает библиотека cats effect.
</aside>
</section>
<section>
<h3>Cats Effect</h3>
<p>"Высокопроизводительная асинхронная компонуемая платформа для создания приложений в чистом функциональном стиле"</p>
<aside class="notes">
Вот какое определение красуется на главной странице ресурса:
Cats Effect - это "Высокопроизводительная асинхронная компонуемая платформа для создания приложений в чистом функциональном стиле".
Она предоставляет инструмент, известный как «IO монада», ...
</aside>
</section>
<section>
<h3>IO monad</h3>
<ul>
<li>Безопасное использование и управление ресурсами</li>
<li>Типизированность</li>
<li>Параллельность (Fiber - легковесные потоки, управляемые средой выполнения)</li>
<li>Асинхронность (callback-driven) или синхронность</li>
<li>Конечное или бесконечное время выполнения</li>
</ul>
<aside class="notes">
... который позволяет управлять эффектами и следить за жизненным циклом ресурсов, безопасно выделять и освобождать их.<p>
При этом эффекты могут быть как асинхронными (т.е. вызывать callback-функцию по окончании действия)
так и синхронными (т.е. непосредственно возвращающими значения).<p>
Параллелизму здесь способствует понятие "волокон",
которые представляют собой легковесные прерываемые потоки, полностью управляемые средой выполнения.<p>
Это вольный перевод, в оригинале они называются: Fiber.<p>
Эти волокна намного дешевле, чем нативные потоки операционной системы, поэтому можно создавать их в огромном количестве.<p>
Интересной особенностью IO-монады является её способность не только выполнить какое-то действие и вернуть результат, но и выпоняться бесконечно.
Дальше мы увидим подобные примеры.<p>
Стоит также сказать, что IO-монада является типизированной, но этим уже мало кого удивишь.
</aside>
</section>
<section>
<h3 style="text-transform: none;">IO vs Future</h3>
<aside class="notes">
Несмотря на внешнее сходство с Future, IO имеет несколько существенных отличий,
которые заметно расширяют возможности разработчика контролировать поведением асинхронных вычислений.<p>
Чтобы увидеть это, давайте глубже познакомимся с синтаксисом
</aside>
</section>
<section>
<pre><code class="scala"> object Future {
def apply[A](body: => A): Future[A]
}</code></pre>
<pre><code class="scala"> object IO {
def apply[A](body: => A): IO[A]
}</code></pre>
<aside class="notes">
Для начала посмотрим на конструктор.
На первый взгляд, они абсолютно одинаковые: в обоих случаях мы передаёт тело функции как by-name параметр.
(т.е. оно будет выполнено только в момент непосредственного использования).
Но внутри Future сразу запускает вычисление и возвращает некоторую сущность,
которая по окончании вычисления будет содержать полученное значение.
В дальнейшем мы можем обращаться к ней несколько раз и получать это значение.<p>
IO действует иначе.
Возвращается контейнер с функцией, но вычисление ещё не запускалось.
Оно будет запущено только когда мы попытаемся получить значение.
При этом если мы будем обращаться к значению несколько раз, то оно будет вычисляться заново!
</aside>
</section>
<section>
<table>
<tr>
<td></td>
<td><b>Eager<br><small>with Memo</small></b></td>
<td><b>Lazy<br><small>with Memo</small></b></td>
<td><b>Lazy<br><small>without Memo</small></b></td>
</tr>
<tr>
<td><b>Sync</b></td>
<td>val<br><small>A</small></td>
<td>lazy val<br><small>() => A</small></td>
<td>def<br><small>() => A</small></td>
</tr>
<tr>
<td><b>Async</b></td>
<td>Future[A]<br><small>(A => Unit) => Unit</small></td>
<td></td>
<td>IO[A]<br><small>() => (A => Unit) => Unit</small></td>
</tr>
</table>
<aside class="notes">
Можно провести аналогию с val и def:
val сразу вычисляет значение и возвращает его, сколько бы раз мы не обращались.
def вычисляет только при обращении, причём делает это каждый раз.
В случае future и io происходит тоже самое, но вычисляется в другом потоке.
<p>
Таким образом, в отличие от Future, IO представляет собой ОПИСАНИЕ куска программы, а не текущие вычисления.
Это дает полный контроль над тем, как и когда будут выполняться эффекты.
Простые программы могут быть использованы для составления более сложных программ,
сохраняя при этом своё поведение и сложность.
</aside>
</section>
<section>
<div><img src="Dynamite Effects.png"></div>
<aside class="notes">
Работа с Future при написании программы напоминает попытку собрать сложное устройство из заранее запущенных механизмов.
Если запустить сразу много потоков, или действовать неаккуратно,
можно легко получить проблему и непредсказуемый результат.<p>
IO позволяет нам конструировать программу из действий, которые ещё не запущены,
но будут выполнены в дальнейшем по нашей команде.
</aside>
</section>
<section>
<div>IO evaluated at the "end of the world"</div>
<aside class="notes">
Обычно говорят, что IO запускается "at the end of the world
(т.е. подчёркивается, что мы сначала всё конструируем, а запускаем в самом конце)
</aside>
</section>
<section>
<pre><code class="scala"> val addToGauge = IO {
???
println("Added!")
}
val program: IO[Unit] =
for {
_ <- addToGauge
_ <- addToGauge
} yield ()
program.unsafeRunSync()
// Added!
// Added!</code></pre>
<aside class="notes">
Вот как это обычно выглядит на практике.
Представьте, что у вас есть какие-то кусочки программы, завёрнутые в IO.
Например, кусок кода, который увеличивает счётчик и печатает сообщение об этом.
<p>
Воспользуемся тем, что IO - это монада, а значит у неё есть метод flatMap,
доступны различные трансформеры, которые позволяют "накручивать" конструкцию
и конечно же можно использовать красивый и наглядный синтаксис for.
<p>
Когда конструкция будет собрана, то в конце запускается метод run и получается результат.
<p>
Обратите внимание, что в данном случае метод print выполнился два раза.
Если бы мы использовали Future, то счётчик был бы увеличен только один раз.
</aside>
</section>
<section>
<div>Stack Safety</div>
<aside class="notes">
Ещё одно существенное отличие от Future - это безопасность относительно переполнения стека.<p>
Возьмём классический пример: вычисления чисел Фиббоначи.
</aside>
</section>
<section>
<pre><code class="scala"> def fib(n: Int, a: Long = 0, b: Long = 1): IO[Long] =
IO(a + b).flatMap { b2 =>
if (n > 0)
fib(n - 1, b, b2)
else
IO.pure(a)
}</code></pre>
<div class=fragment>IO is <b>trampolined</b> in its <b>flatMap</b> evaluation</div>
<aside class="notes">
В случае с IO мы можем записать его в таком виде.
Здесь используется рекурсия, но не хвостовая: последним действием здесь является flatMap.
Для Future это вызвало бы Stack Overflow, но для IO такая конструкция допустима.
<hr>
Такое поведение называется "trampoline" относительно flatMap.
</aside>
</section>
<section>
<h3>Trampolining</h3>
<p>Основная идея – сделать, чтобы функция возвращала continuation</p>
<aside class="notes">
Что же такое trampoline и как это вообще работает?<p>
Давайте вспомним, что при каждом вызове функции выделяется дополнительная память
(как минимум на переменные, передаваемые в функцию).<p>
При глубокой рекурсии в стеке вызовов оказывается гораздо больше информации, чем он может вместить.
В итоге это вызывает "переполнение стека".<p>
Хвостовая рекурсия решает эту проблему за счёт того, что последним действием вызывает сама себя
и это позволяет трансформировать её в цикл, а не создавать более глубокий стек.<p>
Здесь идея похожая. Нужно сделать так, чтобы функция в итоге возвращала либо окончательный результат вычислений,
либо continuation. Это должна быть функция без аргументов, содержащую оставшуюся часть вычислений.
</aside>
</section>
<section>
<pre><code class="scala">sealed abstract class IO[A]
case class Pure[A](a: A) extends IO[A]
case class Suspend[A](thunk: () => A) extends IO[A]
case class FlatMap[A, B](io: IO[B], f: B => IO[A]) extends IO[A]</code></pre>
<aside class="notes">
Достигается это за счёт введения классов-наследников от IO.<p>
Реализуется 3 вида наследника:<br>
готовый результат (Pure),<br>
отложенное вычисление (Suspend)<br>
и FlatMap, который представляет из себя пару из вычисления и его продолжения.
</aside>
</section>
<section>
<pre><code class="scala"> sealed abstract class IO[A] {
def flatMap[B](f: A => IO[B]): IO[B] = FlatMap(this, f)
def unsafeRun(): A = this match {
case Pure(a) => a
case Suspend(thunk) => thunk()
case FlatMap(io, f) => f(io.unsafeRun()).unsafeRun()
}
}</code></pre>
<aside class="notes">
Теперь мы можем определить flatMap как метод, который всегда возвращает результат вычислений - объект класса FlatMap.<p>
А метод запуска вычисления итоговой IO тогда будет выглядеть как pattern-matching.<p>
На первый взгляд может показаться, что мы просто перенесли всю рекурсию из метода flatMap в метод run,
но если расписать внутренний вызов unsafeRun ...
</aside>
</section>
<section>
<pre><code class="scala"> def unsafeRun(): A = this match {
case Pure(a) => a
case Suspend(thunk) => thunk()
case FlatMap(ioA, f) => ioA match {
case Pure(a) =>
f(a).unsafeRun()
case Suspend(thunk) =>
thunk().flatMap(f).unsafeRun()
case FlatMap(ioB, g) =>
ioB.flatMap(g(_) flatMap f).unsafeRun()
}
}</code></pre>
<p>Получаем хвостовую рекурсию!</p>
<aside class="notes">
... то видно, что это не что иное как хвостовая рекурсия.<p>
Это можно наблюдать на текущем слайде.<p>
Разумеется, приведённый здесь код - это лишь упрощённый вариант реализации этих методов.
Здесь отсутствует обработка ошибок, не поддерживаются асинхронные эффекты и т.д.
Но он наглядно иллюстрирует как реализуется trampoline в Cats Effect.
</aside>
</section>
<section>
<h3>IO API</h3>
<aside class="notes">
Давайте теперь немного погрузимся в API, которое предоставляет библиотека.
</aside>
</section>
<section>
<pre><code class="scala"> object IO {
//side effect is not thread-blocking:
def apply[A](thunk: => A): IO[A] //alias for delay
def delay[A](thunk: => A): IO[A]
//side effect is thread-blocking:
def blocking[A](thunk: => A): IO[A] //uncancelable
def interruptible[A](thunk: => A): IO[A] //cancelable
def interruptibleMany[A](thunk: => A): IO[A]
}</code></pre>
<aside class="notes">
Помимо стандартного конструктора, который принимает by-name параметром отложенное вычисление,
существуют и другие способы создать IO.<p>
Например, метод есть метод delay, который имеет чуть более говорящее название, но на самом деле это просто его синоним.<p>
Эти конструкторы подходят только для неблокирующих операций.<p>
Для блокирующих нужно использовать либо конструктор blocking, который является непрерываемым,<br>
либо прерываемый interruptible.<p>
Обратите внимание, что для прерывания interruptable будет предпринята только одна попытка,<br>
в то время как его разновидность interruptibleMany будет получать повторяемые попытки прерывания
до тех пор, пока блокирующая операция не завершится или не выйдет.<p>
Все перечисленные конструкторы имеют одинаковую семантику и отличие только в логике работы
(и, соответственно, в производительности).<br>
Например, interruptable будет работать заметно медленнее, чем blocking,
потому что имеет дополнительный оверхед от координации прерываний.
</aside>
</section>
<section>
<pre><code class="scala"> object IO {
//was `async` in Cats Effect 2.x
def async_[A](
k: ((Either[Throwable, A]) => Unit) => Unit
): IO[A]
//generalized version for `cancelable` in Cats Effect 2.x
def async[A](
k: ((Either[Throwable, A]) => Unit) => IO[Option[IO[Unit]]]
): IO[A]
}</code></pre>
<aside class="notes">
Если обычные IO .apply и .delay описывают операции, которые могут сразу же быть выполнены в том же треде и call-стеке,
то для описания операций в других потоках нужно использовать IO.async.<p>
В старых версиях cats-effect для данного метода было необходимо описать последовательность действий,
которая может принимать в качестве параметра callback.
Сам callback имеет сигнатуру функции, применяемой к "Either Throwable A", но ничего не возвращающей.<p>
Т.е. описывая действия мы можем ещё и вызывать callback-и.<p>
В версии Cats Effect 3 и выше метод был переименован в async с подчёркиванием и предлагается использовать новую сигнатуру.<p>
Теперь описываемая последовательность должна возвращать IO от Option.<br>
В Option содержится необязательный финализатор, который будет запущен в случае отмены файбера, выполняющего метод async.<p>
В версии 2 для этих целей использовался отдельный конструктор cancelable, а теперь их обобщили в один.<p>
Внешний IO здесь нужен для того, чтобы приостановить процесс регистрации самого обратного вызова.
</aside>
</section>
<section>
<pre><code class="scala">def fromCompletableFuture[A](f: IO[CompletableFuture[A]]):IO[A]=
f.flatMap { cf =>
IO.async { cb =>
IO {
//Invoke the callback with the result
//of the completable future
val stage = cf.handle[Unit] {
case (a, null) => cb(Right(a))
case (_, e) => cb(Left(e))
}
//Cancel the completable future if the fiber is canceled
Some(IO(stage.cancel(false)).void)
}}}</code></pre>
<aside class="notes">
Например, давайте посмотрим на упрощённую реализацию метода fromCompletableFuture.<p>
Когда фьюча завершается, будет вызван навешенный колбек.<br>
При этом в него будет передан Either от успеха или ошибки выполнения фьючи.<p>
В качестве возвращаемого значения мы видим здесь действие, которое может отменить вызов колбека.
</aside>
</section>
<section>
<pre><code class="scala">object IO {
def pure[A](value: A): IO[A] //already evaluated
def canceled: IO[Unit] //already cancelled
def raiseError[A](t: Throwable): IO[A] //already throwed
def stub: IO[Nothing]
def unit: IO[Unit] //alias for IO.pure(())
def none[A]: IO[Option[A]] //contains None
def some[A](a: A): IO[Option[A]] //contains Some(a)
def raiseUnless(cond: Boolean)(e: => Throwable): IO[Unit]
def raiseWhen(cond: Boolean)(e: => Throwable): IO[Unit]
def never[A]: IO[A] //alias for async(_ => ())
}</code></pre>
<aside class="notes">
Разумеется есть и вырожденные варианты для создания IO-шек.<p>
Например, pure, canceled и raiseError создают IO, получая параметр не по имени, а по значению.
Таким образом подставляется уже вычисленное значение, ошибка или создаётся изначально прерванную IO.<p>
Конструктор some действует аналогично pure, но дополнительно оборачивает значение в Some.
Есть также совсем вырожденные случаи stub, unit и none.<p>
Более интересные варианты: raiseUnless и raiseWhen,<br>
- которые очень часто используются на практике для проверки условий и прерываний последовательности действиу внутри for.<p>
Отдельно стоит обратить внимание на IO.never. Это действие, которое никогда не завершится.<br>
На самом деле это просто алиас для async, который ничего не делает и никогда не вызывает callback,<br>
но он имеет очень интересное практическое значение и способы применения, которые мы увидим дальше.
</aside>
</section>
<section>
<pre><code class="scala"> object IO {
def fromEither[A](e: Either[Throwable, A]): IO[A]
def fromFuture[A](fut: IO[Future[A]]): IO[A]
def fromOption[A](o: Option[A])(orElse: => Throwable): IO[A]
def fromTry[A](t: Try[A]): IO[A]
}</code></pre>
<aside class="notes">
Также есть методы для трансформирования в IO из других типов, которые сводятся к одному из двух вариантов: успех или не успех.
Например, Either, Future, Option или Try
</aside>
</section>
<section>
<pre><code class="scala">class IO[A] {
def map[B](f: A => B): IO[B]
def flatMap[B](f: A => IO[B]): IO[B]
def redeem[B](recover: Throwable => B, map: A => B): IO[B]
def redeemWith[B](r: Throwable => IO[B], b: A => IO[B]): IO[B]
def as[B](newValue: => B): IO[B] = map(_ => newValue)
def void: IO[Unit] = map(_ => ())
}</code></pre>
<aside class="notes">
Наиболее интересны на практике методы, которые позволяют трансформировать уже существующие IO
или комбинировать новую IO из нескольких существующих.<p>
Например, как вы уже догадались, есть методы map и flatMap.<p>
Также есть методы redeem и redeemWith, напоминающие fold.<p>
И есть методы as и void, которые являются частными случаями метода map, когда результат вычисления не важен.
</aside>
</section>
<section>
<pre><code class="scala"> object IO {
def race[A, B](left: IO[A], right: IO[B]): IO[Either[A, B]]
def racePair[A, B](left: IO[A], right: IO[B]):
IO[Either[
(OutcomeIO[A], FiberIO[B]),
(FiberIO[A], OutcomeIO[B])
]]
}</code></pre>
<aside class="notes">
Интересные методы: race и racePair.<p>
Первый запускает две IO параллельно и удерживает результат той, что выполнится раньше. Опоздавшую останавливает.<p>
При этом результатом будет Either, а значит мы сохраняем информацию о том, какая из них завершилась раньше: left или right.
Также это позволяет иметь разные возвращаемые типы для запускаемых IO-шек.<p>
Второй метод работает аналогично, но он не прерывает "опоздавшую", а возвращает вместо неё Файбер,
который польватель может при желании сам прервать или как-то ещё обработать.<p>
В Cats Effect версии 2 для первой завершённой IO возвращался результат как это делается в методе race,
но начиная с версии 3 возвращается обёрнутый в Outcome результат.<p>
Через пару слайдов я расскажу что это такое, а пока давайте посмотрим на пример.
</aside>
</section>
<section>
<pre><code class="scala"> val ioA: IO[A] = ???
val ioB: IO[String] = IO.sleep(10.seconds).as("Timeout")
IO.racePair(ioB, ioA).flatMap {
case Left((err, fiberA)) =>
fiberA.cancel.as(err)
case Right((_, a)) =>
IO.pure(a)
}</code></pre>
<aside class="notes">
Предположим у нас есть две IO.<p>
Одна из них выполянет какое-то долгое действие.<p>
Вторая ожидает 10 секунд и возвращает текст ошибки.<p>
Запускаем их параллельно.<p>
Если за отведённое время вычисление не случится, то мы его отменяем и возвращаем ошибку.
</aside>
</section>
<section>
<pre><code class="scala"> object IO {
def both[A, B](left: IO[A], right: IO[B]): IO[(A, B)]
def bothOutcome[A, B](left: IO[A], right: IO[B]):
IO[(OutcomeIO[A], OutcomeIO[B])]
}</code></pre>
<aside class="notes">
Есть и метод, который не устраивает гонку, а дожидается оба результата.
Для него есть две разновидности.<p>
Один из них возвращает только пару с успешным результатом.
А когда одно из вычислений заканчивается с ошибкой, то второе прерывается и возвращается эта ошибка.<p>
И второй вариант, который в любом случае возвращает оба результата.<p>
И здесь снова видим результат, обёрнутый в Outcome.
</aside>
</section>
<section>
<h3>Outcome</h3>
<aside class="notes">
Так что же это такое?
Давайте подумаем, как может завершиться вычисление?
Есть три варианта: успех, ошибка или отменённое вычисление.
Именно эту информацию и предоставляет Outcome.
</aside>
</section>
<section>
<pre><code class="scala"> sealed trait Outcome[F[_], E, A]
case class Succeeded[F[_],E,A](s: F[A]) extends Outcome[F,E,A]
case class Errored [F[_],E,A](e: E) extends Outcome[F,E,A]
case class Canceled [F[_],E,A]() extends Outcome[F,E,A]
</code></pre>
<aside class="notes">
Это sealed trait, у которого есть 3 варианта реализации.<p>
Успех удерживает результат вычисления.<p>
Ошибка удерживает информацию об ошибке, чаще всего это какой-нибудь эксепшен.<p>
А отменённому вычисление ничего удерживать не требуется.<p>
Сам трейт предоставляет различные методы для обработки результата.
</aside>
</section>
<section>
<pre><code class="scala"> sealed trait Outcome[F[_], E, A] {
def isCanceled: Boolean
def isError: Boolean
def isSuccess: Boolean
def fold[B](onCancel: => B,
onError: (E) => B,
onComplete: (F[A]) => B
): B
}</code></pre>
<aside class="notes">
Разумеется там есть всякие функции для матчинга результата.<p>
Но наиболее удобная функция - это fold.<p>
Она позволяет обработать сразу все варианты возможных результатов.
</aside>
</section>
<section>
<h3>Resource</h3>
<aside class="notes">
Теперь давайте разберёмся, как библиотека предлагает нам работать с ресурсами.<p>
Самым распространённым шаблоном работы с ресурсами (будь то файл или сокет) является
его получение, выполнение некоторого действия и затем запуск финализатора
(например, закрытие дескриптора файла).
При этом финализатор должен вызываться независим от результата действия.
</aside>
</section>
<section>
<pre><code class="scala"> def bracket[A, B](acquire: F[A])
(use: A => F[B])
(release: A => F[Unit]): F[B]
//acquire & release - uncancelable
//use - cancelable, but could be masked
</code></pre>
<aside class="notes">
Такую логику реализует метод bracket и его разновидности.<p>
Первым параметром он принимает действие получения ресурса,
вторым - действие, которое будет выполнено при успешном получении,
а третьим освобождает ресурс.
При этом последнее будет вызвано при любом Outcome у второго параметра, будь то успех, ошибка или отмена.<p>
Обратите внимание, что действия полуения и освобождения ресурса являются неотменяемыми
и гарантируется вызов финализатора ровно один раз.<p>
Само действие над ресурсом изначально отменяемое, но как и на любую IO на неё можно навесить маску неотменяемости.
</aside>
</section>
<section>
<pre><code class="scala">
IO.bracket(openFile("file1")) { file1 =>
IO.bracket(openFile("file2")) { file2 =>
IO.bracket(openFile("file3")) { file3 =>
for {
bytes1 <- read(file1)
bytes2 <- read(file2)
_ <- write(file3, bytes1 ++ bytes2)
} yield ()
}(file3 => close(file3))
}(file2 => close(file2))
}(file1 => close(file1))
</code></pre>
<aside class="notes">
Однако, комбинация из нескольких подобных вложенных методов быстро становится громоздкой.<p>
Пример можно видеть на данном слайде.
Здесь читаются два файла и их объединение записывается в третий файл.<p>
Вторым недостатком данного метода является смешивание логики получения ресурса и работы с ним.<p>
Для решения обоих этих проблем Cats Effect предлагают использовать отдельный класс - Resource.<p>
Туда выносится вся логика получения и освобождения ресурса, оставляя в стороне всю содержательную часть логики.<p>
Объекты типа resource можно легко комбинировать, избегая громоздких вложенных конструкций вроде той,
что мы наблюдаем для bracket.
</aside>
</section>
<section>
<pre><code class="scala"> object Resource {
def make[F[_], A](acquire: F[A])
(release: A => F[Unit]): Resource[F, A]
def eval[F[_], A](fa: F[A]): Resource[F, A]
}</code></pre><pre><code class="scala"> abstract class Resource[F, A] {
def use[B](f: A => F[B]): F[B]
}</code></pre>
<aside class="notes">
Самым простым способом создать ресурс является метод make, который принимает всё те же параметры
с действиями получения и освобождения ресурса.<p>
Метод для работы с полученным ресурсом вынесен в метод класса.<p>
Можно также построить ресурс из имеющегося аппликатива с помощью метода eval.
При этом подразумевается, что финалайзер никакой не требуется, и на функцию создания не вешается маска непрерываемости.
Т.е. если они была прерываемой, то при выделении ресурса такой и останется.
</aside>
</section>
<section>
<pre><code class="scala"> def file(name: String): Resource[IO, File] =
Resource.make(openFile(name)))(file => close(file))
( for { in1 <- file("file1")
in2 <- file("file2")
out <- file("file3")
} yield (in1, in2, out)
).use { case (file1, file2, file3) =>
for { bytes1 <- read(file1)
bytes2 <- read(file2)
_ <- write(file3, bytes1 ++ bytes2)
} yield ()
}</code></pre>
<aside class="notes">
Вот во что превращается наш пример при использовании ресурса.<p>
Обратите внимание, что ресурсы высвобождаются в порядке, обратном получению.<p>
И ещё раз заметим, что и получение, и освобождение не прерываются и безопасны в случае отмены основной логики.<br>
Таким образом внешние ресурсы будут освобождены в любом случае,
независимо от сбоя в жизненом цикле внутреннего ресурса.
</aside>
</section>
<section>
<pre><code class="scala">
open(file1).use(IO.pure).flatMap(readFile)
// ОШИБКА: файл уже закрыт
</code></pre>
<aside class="notes">
Также обратите внимание, что завершение происходит, как только блок использования завершается.<p>
Поэтому данный код вызовет ошибку, т.к. файл уже закрыт, когда мы пытаемся его прочитать.
</aside>
</section>
<section>
<pre><code class="scala">
file.use(read) >> file.use(read)
// дважды открыли и закрыли
file.use { file => read(file) >> read(file) }
// один раз открыли и закрыли
</code></pre>
<aside class="notes">
Другой пример.<p>
Здесь использован метод "две стрелочки". Это flatMap, который игнорирует результат первого вычисления.<p>
Предположим мы дважды обратились к файлу и прочитали его.<br>
Тогда он дважды был открыт и дважды закрыт.<br>
КАЖДОЕ обращение к ресурсу с методом use вызывает его создание финализацию.<p>
Если требуется открыть файл только один раз, то можно вызвать один раз метод use и обратиться к файлу дважды внутри него.
</aside>
</section>
<section>
<h3>Thread Model</h3>
<p>Fibers</p>
<aside class="notes">
Теперь поговорим подробнее о тред-пулах и файберах.<p>
Как уже упоминалось в начале лекции, в Cats Effect реализована концепция Fiber-ов.
Это такие легковесные потоки, управляемые средой выполнения.<p>
Чтобы понять их работу, необходимо вспомнить как устроены потоки в программах.
</aside>
</section>
<section>
<h3>Логический поток</h3>
<div><img src="CE-Threads-1-colored.png" height="250" style="background: none; border: 0; box-shadow: none;"></div>
<aside class="notes">
Допустим у нас есть некий процесс А, который состоит из некоторых дискретных шагов.
Они последовательно выполняются.<p>
Такой процесс называется логическим потоком.
</aside>
</section>
<section>
<h3>Асинхронный процесс</h3>
<div><img src="CE-Threads-4-colored.png" height="300" style="background: none; border: 0; box-shadow: none;"></div>
<aside class="notes">
Логический поток может быть разбит на две части некоторой асинхронной границей.<p>
Например, часть инструкций может выполняться на каком-то одном узле кластера,
потом происходит передача данных по сети и вторая часть инструкций выполняется уже на другом узле.<p>
Можно рассматривать процессы и на более низком уровне.
Например, это уже не узлы кластера, а разные потоки операционной системы и часть инструкций шедулятся на одном потоке, а часть на другом.<p>
Асинхронный процесс можно рассматривать как процесс,
продолжающий своё выполнение в другом месте по отношению к тому, где он стартовал.<p>
</aside>
</section>
<section>
<h3>Перемешивание</h3>
<div><img src="CE-Threads-2-colored.png" height="400" style="background: none; border: 0; box-shadow: none;"></div>
<aside class="notes">
Дискретность в логических потоках позволяет нам перемешивать несколко процессов и выполнять их на одном потоке более низкого уровня.
Такое перемешивание обеспечивает некоторая компонента, которая называется "планировщик".
</aside>
</section>
<section>
<h3>M:N Threading</h3>
<div><img src="CE-Threads-3-colored.png" height="400" style="background: none; border: 0; box-shadow: none;"></div>
<aside class="notes">
Если у нас есть несколько логических потоков и несколько потоков более низкого уровня,
то мы приходим к схеме, называемой m:n threading.<p>
При этом подразумевается, что на более высоком уровне потоков будет больше,
иначе в этом нет ни смысла, ни какого-либо выигрыша.
</aside>
</section>
<section>
<p><small>Логический поток предоставляет<br>синхронный интерфейс<br>к асинхронному процессу</small></p>
<div><img src="CE-Threads-5-colored.png" height="400" style="background: none; border: 0; box-shadow: none;"></div>
<aside class="notes">
Также говорят о том, что логический поток предоставляет синхронный интерфейс к асинхронному процессу.<p>
Что это значит?<p>
Вот у нас есть 3 логических потока: A, B и C.<br>
Они синхронные.<p>
При работе шедулера их фрагменты раскидываются по разным процессам и в итоге наш поток А выполняется асинхронно.
</aside>
</section>
<section>
<div><img src="CE-Threads-6-colored.png" height="400" style="background: none; border: 0; box-shadow: none;"></div>
<aside class="notes">
Более того, даже если у нас будет только один поток, то фрагменты логического потока А будут выполняться
не сразу один за другим, а разнесутся по времени и будут выполняться вперемешку с другими логическими потоками.<p>
Таким образом мы всё равно получаем асинхронное выполнение потока А.
</aside>
</section>
<section>
<div><img src="CE-Threads-7-colored.png" height="400" style="background: none; border: 0; box-shadow: none;"></div>
<aside class="notes">
К чему все эти сложности?<p>
Есть такая штука как блокировка.<p>
Представьте, что логический поток А приостановил свою работу и ожидает выполнение В.<p>
За счёт того, что у нас есть шедулер, который это отслеживает,
мы не останавливаем выполнение потока на системном уровне,
а продолжаем выполнять другие потоки.<p>
Таким образом несмотря на возникновение блокировки на более высоком уровне процессор не "встаёт колом",
а продолжает активно работать и выполнять другие задачи.
</aside>
</section>
<section>
<h3>Уровни</h3>
<dl>
<dt>1. Процессы ОС</dt><small>
<dd>M:N с процессорами.</dd>
<dd>Собственное состояние выполнения, собственное пространство памяти</dd></small>
<dt>2. ОС/JVM Threads</dt><small>
<dd>M:N с процессами.</dd>
<dd>Собственное состояние выполнения, разделяемое пространство памяти</dd></small>
<dt>3. Fibers</dt><small>
<dd>M:N c потоками.</dd>
<dd>Разделяемое состояние выполнение, разделяемое пространство памяти</dd></small>
</dl>
<aside class="notes">
Таким образом мы приходим к следующей иерархии.<p>
На самом низком уровне у нас лежат процессы операционной системы.
Дальше у нас идут потоки JVM и ОС.
И наконец мы приходим к так называемым файберам.<p>
Идея в том, что запуск процессов операционной системы достаточно трудоёмкий и дорогостоящий.<p>
Cats Effect реализует создание файберы размером примерно 150 байт каждый.<p>
Процесс создания и запуска нового файбера сам по себе чрезвычайно быстр,
что позволяет создавать очень недолговечные, «одноразовые» волокна, когда это удобно.<p>
И вы можете без проблем создавать их миллионами, а вашим основным ограничивающим фактором будет просто память.<p>
Запуск файберов находится на пользовательском уровне и обеспечивает нам дополнительный уровень в иерархии,
который позволяет нам на этом уровне осуществлять синхронную блокировку,
но при этом не блокировать лежащие в их основе потоки JVM.<p>
Это так называемые семантические блокировки.
</aside>
</section>
<section>
<h3>Кооперативное планирование</h3>