Пока значительная часть Scala-community сходит с ума от монад всех сортов и расцветок, некоторые отмечают недостаток выразительности существующего синтаксиса для работы с ними. Одним из решений является библиотека
Сайт
http://monadless.io/
monadless.io
, позволяющая работать вне контекста монады с помощью функции unlift
и возвращаться в него с помощью метода lift
. Бонусом - интеграция с инфраструктурой библиотек cats
, monix
и algebird
.Сайт
monadless.io
содержит достаточное количество примеров, покрывающих наиболее частые случаи использования, а также подробное объяснение того, как работает внутренняя машинерия библиотеки.http://monadless.io/
Ни для кого не секрет, что механизм
Осознаёт сложившуюся ситуацию и лидер Scala-сообщества Мартин Одерски. В новой версии языка (aka Scala 3) он предлагает полностью заменить неявные определения механизмом
https://github.com/lampepfl/dotty/tree/174b45edcf13c53597c87e46345418d86e95d396/docs/docs/reference/instances
implicit
-ов в Scala - чрезвычайно мощный инструмент, позволяющий писать легкочитаемый, не перегруженный код. Тем не менее, как и многие инструменты с большим числом возможностей, он может послужить обратной цели, если использовать его неправильно. В дополнение, сложность выведения некоторых implicit
-ов в значительной мере затрудняет их восприятие.Осознаёт сложившуюся ситуацию и лидер Scala-сообщества Мартин Одерски. В новой версии языка (aka Scala 3) он предлагает полностью заменить неявные определения механизмом
instance
-ов, более простым в обращении и при этом ограничивающим программиста от написания плохого кода. Более подробно о мотивации и синтаксисе новой конструкции языка можно почитать в репозитории компилятора Dotty
:https://github.com/lampepfl/dotty/tree/174b45edcf13c53597c87e46345418d86e95d396/docs/docs/reference/instances
GitHub
lampepfl/dotty
The Scala 3 compiler, also known as Dotty. Contribute to lampepfl/dotty development by creating an account on GitHub.
Вдогонку к приятной новости про
В целом, предложение содержит полную переработку механизма обращения с
Полное описание предложения можно найти по ссылке:
https://gist.github.com/abeln/9f79774bac111d99b3ae2cb9016a33e6
implicit
-ы (как некоторые заметили, очередной шаг в направлении языка Haskell
) в Scala 3 было внесено не менее масштабное предложение - полностью исключить null
из значений отдельных типов и обособить его в свой собственный Null
наряду с уже существующими AnyVal
(примитивы) и AnyRef
(объекты). В целом, предложение содержит полную переработку механизма обращения с
null
-значениями на уровне компилятора. Основная идея заключается в присвоении всем потенциально аnull
ируемым значениям типа TypeName
|
Null
, в связи с чем их уже нельзя будет передать в методы, принимающие строго тип TypeName
. Из интересных особенностей - уточнение типа переменных при сравнении с null
на стабильных путях (привет, Kotlin
!). При этом авторы также основательно поработали над взаимодействием с кодом предыдущих версий языка Scala и Java-библиотеками, благодаря чему уже существующий код потребует лишь незначительных изменений.Полное описание предложения можно найти по ссылке:
https://gist.github.com/abeln/9f79774bac111d99b3ae2cb9016a33e6
Gist
Scala with Explicit Nulls
Scala with Explicit Nulls. GitHub Gist: instantly share code, notes, and snippets.
В качестве одного из основных аргументов в пользу функционального программирования часто приводится абстрактное "оно способствует написанию хорошего кода". Понять, про какой код идёт речь и почему именно он хороший можно на примере обычного http-сервиса.
В классическом рассмотрении такой сервис будет являться в первую очередь сервером, для которого будут определены доступные url и соответствующие им обработчики запросов. Зачастую также заданы (по умолчанию) обработчики серверных ошибок, позволяющие дать ответ на запрос даже при возникновении ошибочной ситуации, а механизмы аутентификации и снятия метрик представлены в виде отдельных модулей-библиотек.
С точки зрения ФП такой сервис будет являться просто функцией
Более подробно (с примерами кода) в видео с митапа
https://www.youtube.com/watch?v=0oVpLdgZqpE
В классическом рассмотрении такой сервис будет являться в первую очередь сервером, для которого будут определены доступные url и соответствующие им обработчики запросов. Зачастую также заданы (по умолчанию) обработчики серверных ошибок, позволяющие дать ответ на запрос даже при возникновении ошибочной ситуации, а механизмы аутентификации и снятия метрик представлены в виде отдельных модулей-библиотек.
С точки зрения ФП такой сервис будет являться просто функцией
Request => Response
. Из этого сразу вытекает, что взаимодействовать с таким сервисом можно даже без наличия реального сервера, и, более того, к нему применимы те же рассуждения, что и к обычным функциям. Путём модификации сигнатуры функции сервиса в него можно добавить, к примеру, обработку внутренних исключений, а с помощью комбинации функций можно добавить в сервис метрики, логирование, аутентификацию и т.д. Именно тот факт, что высокоуровневую абстракцию сервиса можно выразить с помощью функции - базовой конструкции языка - приводит к написанию тестируемого, композабельного и понятного кода, который можно назвать хорошим.Более подробно (с примерами кода) в видео с митапа
Scale by the bay
:https://www.youtube.com/watch?v=0oVpLdgZqpE
YouTube
Scale By The Bay: Paul Snively, FP Scala Meat & Potatoes...
ai.bythebay.io Nov 2025, Oakland, full-stack AI conference Scale By the Bay 2019 is held on November 13-15 in sunny Oakland, California, on the shores of Lake Merritt: https://scale.bythebay.io. Join us!
-----
Title: FP Scala Meat & Potatoes: HTTP, JSON…
-----
Title: FP Scala Meat & Potatoes: HTTP, JSON…
Существует достаточно много объяснений, что же такое монада, как формальных, так и отсылающих к жизненным аналогиям. В числе последних - буррито. Да, то самое мексиканское блюдо из лепёшки и произвольной начинки.
Проводить подобную параллель пытались далеко не в одной статье, но, пожалуй, один из самых правдопободных вариантов принадлежит Марку Доминусу. Это транслируется в термины Scala следующим образом:
1. Операция
2. Операция
3. Операции
Для функториальной составляющей есть даже иллюстрация:
Проводить подобную параллель пытались далеко не в одной статье, но, пожалуй, один из самых правдопободных вариантов принадлежит Марку Доминусу. Это транслируется в термины Scala следующим образом:
1. Операция
pure
: всё, что угодно, мы можем завернуть в лепёшку и получить буррито. 2. Операция
map
: не меняя лепёшки, мы можем поменять начинку и остаться с буррито.3. Операции
flatMap
/flatten
: если сделать гигантский буррито, состоящий из вложенных в него буррито, то, последовательно развернув внутренние составляющие и скомбинировав начинки, мы вновь получим буррито.Для функториальной составляющей есть даже иллюстрация:
The Universe of Discourse : Monads are like burritos
Monads are like burritos
From the highly eclectic blog of Mark Dominus
Если для вас всё это не делает абсолютно никакого смысла, настоятельно рекомендую ознакомиться с книгой
Haskell programming
, где к концепции монады походят очень обстоятельно и без кулинарных ассоциаций:Довольно часто в приложениях какой-то конкретный
Естественно, что ограничения подобного рода хочется выразить не только в документации к проекту и именах переменных, но и в системе типов, чтобы обеспечить их проверку компилятором. Некоторые языки (Agda, Idris, Whiley) позволяют создание зависимых типов из коробки, а такие языки как Haskell и Scala обеспечивают их поддержку на уровне библиотек.
Самой популярной библиотекой подобного плана в Scala является Refined, вдохновленная одноименным аналогом из Haskell. Суммарно в библиотеке поддерживается несколько десятков операций над типами. Вот так, к примеру, с её помощью будет записан тип строки, представляющей из себя непустой набор цифр:
Int
на самом деле представляет собой вовсе не произвольное число, а элемент из некоторого диапазона, не совпадающего со множеством всех возможных значений. Аналогичным образом это относится и к строкам, которые должны иметь определённый формат или попросту не быть пустыми.Естественно, что ограничения подобного рода хочется выразить не только в документации к проекту и именах переменных, но и в системе типов, чтобы обеспечить их проверку компилятором. Некоторые языки (Agda, Idris, Whiley) позволяют создание зависимых типов из коробки, а такие языки как Haskell и Scala обеспечивают их поддержку на уровне библиотек.
Самой популярной библиотекой подобного плана в Scala является Refined, вдохновленная одноименным аналогом из Haskell. Суммарно в библиотеке поддерживается несколько десятков операций над типами. Вот так, к примеру, с её помощью будет записан тип строки, представляющей из себя непустой набор цифр:
MatchesRegex[W.`"[0-9]+"`.T]
. Несмотря на (порой значительное) увеличение времени компиляции, библиотека однозначно рекомендуется к ознакомлению.GitHub
GitHub - agda/agda: Agda is a dependently typed programming language / interactive theorem prover.
Agda is a dependently typed programming language / interactive theorem prover. - agda/agda
Недавно в канале промелькнула новость о предложении убрать в новой версии Scala слово
Помимо этого в стандартную библиотеку был добавлен автоматический вывод type class-ов, что позволит не тянуть в свой проект такую тяжеловесную библиотеку как
Также появилась возможность писать определения типов/методов/значений на самом верхнем уровне, без упаковки в
Более детально посмотреть список последних изменений и прочитать инструкцию по установке третьей версии Scala можно здесь:
https://dotty.epfl.ch/blog/2019/03/05/13th-dotty-milestone-release.html
implicit
как таковое. И вот, implicit
-ы были полностью убраны из третьей версии языка. В каждом из специфичных случаев использования implicit
(которых набралось целых 6 штук) теперь будет использоваться отдельная конструкция. Помимо этого в стандартную библиотеку был добавлен автоматический вывод type class-ов, что позволит не тянуть в свой проект такую тяжеловесную библиотеку как
Shapeless
и ускорить время компиляции.Также появилась возможность писать определения типов/методов/значений на самом верхнем уровне, без упаковки в
object
/class
/trait
, в связи с чем конструкция package object
потеряла смысл и вскоре будет удалена из языка.Более детально посмотреть список последних изменений и прочитать инструкцию по установке третьей версии Scala можно здесь:
https://dotty.epfl.ch/blog/2019/03/05/13th-dotty-milestone-release.html
Сегодня понадобилось сделать "typeclass"-аналог натурального преобразования со следующей сигнатурой:
К сожалению, эта затея не увенчалась успехом - Scala не поддерживает определение "недоприменённых" типов через ключевое слово
trait LiftF[F[_], G[_]] {Естественно, каждый раз писать в контекстных ограничениях что-то вроде
def liftF[A](fa: F[A]): G[A]
}
F[_]: LiftF[?[_], G]]
уже довольно удобно, но хочется иметь возможность делать это максимально лаконично, к примеру, так: F[_]: LiftF.To[G]
. В данном случае тип To[G[_]]
после применения к G
возвращал бы type-лямбду LiftF[?[_], G]
, то есть, по сути, являлся бы аналогом частично применённого конструктора типов из Haskell.К сожалению, эта затея не увенчалась успехом - Scala не поддерживает определение "недоприменённых" типов через ключевое слово
type
- все параметры должны быть вынесены в сигнатуру типа. При этом использование аналога Aux паттерна приводит к ещё более неудобному синтаксису:trait To[G[_]] {Тем не менее, эта заметка не появилась бы в канале, если бы этим всё и ограничилось. Оказывается, в Scala 3 уже есть возможность записывать частично-применённые типы без каких-либо плагинов и библиотек. Вот так в Dotty будет выглядеть реализация типа
type From[F[_]] = LiftF[F, G]
}
def func[A, G[_], F[_]: LiftF.To[G]#From](a: F[A]): G[A]
To
:type To[G[_]] = [F[_]] => LiftF[F, G]Таким образом, появилась ещё одна отличная причина ждать/уже сейчас пробовать Dotty.
Изначально планировал, что на месте этого поста будет сравнение двух подходов к реализации полиформизма: композиции и наследования. Но, пока искал материал, наткнулся на гораздо более животрепещущий топик.
Одним из самых острых вопросов для jvm-программистов на сегодняшний день является уменьшение количества аллокаций в системе. Если не придавать значение числу создаваемых объектов, то можно столкнуться с небезызвестными "GC-подвисаниями", когда сборщик мусора забирает всё процессорное время для удаления ненужных объектов.
Естественно, данная проблема не обошла и Scala. Поскольку в языке нет примитивов, количество используемой памяти могло бы возрастать непозволительно быстро. К счастью, "волшебный" trait
На самом деле, это, конечно, работает далеко не всегда. В документации выделяют три случая, когда обёртка (
1.
2.
3. Проверка типа
Максимально подробные тесты можно найти по ссылке, однако в качестве простого примера, когда объект-обёртка будет создан (по правилу 1), можно привести следующий код:
Одним из самых острых вопросов для jvm-программистов на сегодняшний день является уменьшение количества аллокаций в системе. Если не придавать значение числу создаваемых объектов, то можно столкнуться с небезызвестными "GC-подвисаниями", когда сборщик мусора забирает всё процессорное время для удаления ненужных объектов.
Естественно, данная проблема не обошла и Scala. Поскольку в языке нет примитивов, количество используемой памяти могло бы возрастать непозволительно быстро. К счастью, "волшебный" trait
AnyVal
, наследуемый такими типами как Int
, Long
и т.д., позволяет превратить любой класс с одним value
-параметром в обёртку, которая во время исполнения программы не инстанцинируется (все методы такого класса становятся статическими).На самом деле, это, конечно, работает далеко не всегда. В документации выделяют три случая, когда обёртка (
value
-класс) всё-таки превращается в полноценный объект:1.
Value
-класс рассматривается как объект другого типа.2.
Value
-класс помещён в массив3. Проверка типа
value
-класса во время исполнения программы (например, паттерн матчинг)Максимально подробные тесты можно найти по ссылке, однако в качестве простого примера, когда объект-обёртка будет создан (по правилу 1), можно привести следующий код:
object Test {К счастью (нет, я не специально выбираю такие темы), в Scala 3 появятся
case class Value(value: Int) extends AnyVal
def identity[A](a: A): A = a
println(identity(Value(1))
}
Opaque types
- конструкция языка, гарантирующая отсутствие обёртки при исполнении программы. На текущий же момент рекомендуется просто с умом использовать AnyVal
(или, например, такие конструкции, как типы с тэгами) и прежде всего тестировать, какое реальное влияние оказывает AnyVal
на быстродействие в программе.Medium
Are you sure your AnyVals don’t instantiate?
Verifying whether using Value Classes for domain modelling and communication actually makes sense.
Был удивлён, что при всём обилии возможностей Scala и нарицательном названии "better Java", всё ещё встречаются случаи, когда в полноценный Scala проект приходится-таки вносить
Scala поддерживает аннотации с помощью абстрактного класса
Зачем вообще может понадобиться доступ к аннотациям во время работы программы? К примеру, в тестах: допустим, вы используете модную библиотеку, основанную на
Поскольку аннотации Scala после запуска видны не будут, необходимо создавать Java-аналог. Выглядеть он будет примерно так:
В противовес этому досадному недоразумению в библиотеке
HorribleClass.java
. Один из таких случаев - написание runtime-аннотаций. Scala поддерживает аннотации с помощью абстрактного класса
Annotation
. Используется этот механизм не очень часто, но встретить можно - например, как инструмент для (де)сериализации JSON. До времени исполнения такие аннотации не доживают и стираются компилятором.Зачем вообще может понадобиться доступ к аннотациям во время работы программы? К примеру, в тестах: допустим, вы используете модную библиотеку, основанную на
JUnit
. При этом вы хотите пометить определённые тестовые пакеты (которые представляются в виде классов) как интеграционные, чтобы запускать их отдельно от юнит-тестов. Для этого необходимо использовать аннотации, которые JUnit
проанализирует уже после запуска тестов.Поскольку аннотации Scala после запуска видны не будут, необходимо создавать Java-аналог. Выглядеть он будет примерно так:
@org.scalatest.TagAnnotationСамое печальное, что это сделано намеренно - в документации отдельно описывается разметка тестовых пакетов Java-аннотациями. Разметку же отдельных тестов можно делать средствами Scala, но при большом их количестве это становится крайне утомительно.
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface IntegrationTest {}
В противовес этому досадному недоразумению в библиотеке
specs2
в этом плане всё гораздо более положительно - можно задавать произвольную иерархию тестов, которая к тому же не обязательно должна быть древовидной. Тем не менее (возможно по причинам исторического характера) используется эта библиотека в 7 раз реже (согласно mvnrepository.com).GitHub
circe-derivation/JsonCodec.scala at master · circe/circe-derivation
Fast type class instance derivation for Circe. Contribute to circe/circe-derivation development by creating an account on GitHub.
Наконец-то дошли руки присоединиться к хабра-сообществу: опубликовали мой перевод поста Луки Якобовича "A tale on semirings".
Я выбрал для перевода именно этот пост, поскольку мне очень понравилась наглядность примера - переложение структуры абстрактной алгебры (полукольцо) и связанных с ней законов на код, понятный даже не знакомому со Scala человеку. В дополнение к этому показывается, как можно перенести концепцию полукольца на типы высшего порядка. И всё это с отсылками на классы
Надеюсь, вам понравится как мой перевод, так и сам пост Луки. Несмотря на сравнительно большой объём, материал интересный и изложен достаточно подробно.
Я выбрал для перевода именно этот пост, поскольку мне очень понравилась наглядность примера - переложение структуры абстрактной алгебры (полукольцо) и связанных с ней законов на код, понятный даже не знакомому со Scala человеку. В дополнение к этому показывается, как можно перенести концепцию полукольца на типы высшего порядка. И всё это с отсылками на классы
cats
- одной из наиболее используемых в Scala ФП-библиотек.Надеюсь, вам понравится как мой перевод, так и сам пост Луки. Несмотря на сравнительно большой объём, материал интересный и изложен достаточно подробно.
Хабр
Сказ о полукольцах
Привет, Хабр! Предлагаю вашему вниманию перевод статьи "A tale on Semirings" автора Luka Jacobowitz. Когда-нибудь задумывались, почему сумма типов называется суммой типов. Или, может, вы всегда хотели...
На днях промелькнула заметка об оптимизациях, проводимых компилятором Haskell.
Обычно в контексте максимальной производительности упоминают C, C++ или какую-нибудь из версий ASM. Это обусловлено тем, что данные языки максимально "близки к железу": позволяют напрямую работать с памятью, выравнивать байтики в структурах и компилируются напрямую в машинный код для целевой платформы.
При всём обилии оптимизаций в компиляторах таких языков, ускорение программы подразумевает неизменность в логике ее работы. В данном же случае выведение логики программы и, как следствие, возможность оптимизации, ограничены обилием низкоуровневых возможностей - компилятор не может однозначно вывести взаимосвязи между частями программы.
Haskell, в свою очередь, гораздо более структурирован и обладает своим набором оптимизаций перед компиляцией в язык C— (да, "си минус минус"), который впоследствии переводится в целевой язык. Внезапно, в некоторых случаях эти оптимизации позволяют превзойти в производительности аналогичную программу на языке (в данном случае, ATS), компилируемом с помощью gcc или clang.
Естественно, вышесказанное не означает, что Haskell "быстрее C" - интересна возможность использования функциональной природы языка для оптимизации самой структуры програмы. Можно надеяться, что подобный функционал появится и в Dotty - в списке возможностей компилятора строке "whole program optimizer" соответствует статус "в процессе".
Обычно в контексте максимальной производительности упоминают C, C++ или какую-нибудь из версий ASM. Это обусловлено тем, что данные языки максимально "близки к железу": позволяют напрямую работать с памятью, выравнивать байтики в структурах и компилируются напрямую в машинный код для целевой платформы.
При всём обилии оптимизаций в компиляторах таких языков, ускорение программы подразумевает неизменность в логике ее работы. В данном же случае выведение логики программы и, как следствие, возможность оптимизации, ограничены обилием низкоуровневых возможностей - компилятор не может однозначно вывести взаимосвязи между частями программы.
Haskell, в свою очередь, гораздо более структурирован и обладает своим набором оптимизаций перед компиляцией в язык C— (да, "си минус минус"), который впоследствии переводится в целевой язык. Внезапно, в некоторых случаях эти оптимизации позволяют превзойти в производительности аналогичную программу на языке (в данном случае, ATS), компилируемом с помощью gcc или clang.
Естественно, вышесказанное не означает, что Haskell "быстрее C" - интересна возможность использования функциональной природы языка для оптимизации самой структуры програмы. Можно надеяться, что подобный функционал появится и в Dotty - в списке возможностей компилятора строке "whole program optimizer" соответствует статус "в процессе".
GitLab
hsc main · Wiki · Glasgow Haskell Compiler / GHC
The Glorious Glasgow Haskell Compiler.
Обычно в книгах по изучению какого-либо языка типам посвящают одну-две главы или вообще ограничиваются краткими вставками по ходу книги. Для тех, кто давно ждал расширенного руководства по программированию на уровне типов, в этом году появилась книга Thinking with types под авторством Сэнди Магуайра.
На протяжении около 200 с небольшим страниц рассматриваются как уже достаточно популярные темы (типы высшего порядка, частично применённые типы), так и достаточно экзотические - например, вычисление на уровне типов. Практически все примеры приведены на Haskell, но большинство концепций может быть адаптировано и в других языках программирования.
На протяжении около 200 с небольшим страниц рассматриваются как уже достаточно популярные темы (типы высшего порядка, частично применённые типы), так и достаточно экзотические - например, вычисление на уровне типов. Практически все примеры приведены на Haskell, но большинство концепций может быть адаптировано и в других языках программирования.
Leanpub
Thinking with Types
The quintessential guide to type-level programming in Haskell.
Недавно с удивлением узнал, что в Scala есть продолжения (continuations), аналогичные оным в языках Scheme и Ruby. Добавить их можно с помощью специального плагина и флага компилятора.
Продолжения в каком-то роде представляют из себя функциональный аналог goto, позволяя передавать управление вне текущего блока, а потом снова в него возвращаться. Вот так, например, с их помощью можно реализовать простейший итератор в Scala:
Причина забвения проста - чтобы разобраться в коде с их использованием, нужно приложить значительные усилия. К примеру, попробуйте догадаться, что напечатает в консоль вышеприведённый код.
Продолжения в каком-то роде представляют из себя функциональный аналог goto, позволяя передавать управление вне текущего блока, а потом снова в него возвращаться. Вот так, например, с их помощью можно реализовать простейший итератор в Scala:
reset {Относятся к продолжениям неоднозачно - в JRuby, к примеру, их отказались поддерживать. Незавидная участь, по видимому, ожидает их и в Scala - последнее обновление для соответствующего плагина выходило 2 года назад и даже тогда не было заметно его активного использования в проектах.
def iterate() = shift { f: (Int => Int) =>
println("start")
val x = f(0)
println(s"i $x")
x + 1
}
iterate()
iterate()
iterate()
}
Причина забвения проста - чтобы разобраться в коде с их использованием, нужно приложить значительные усилия. К примеру, попробуйте догадаться, что напечатает в консоль вышеприведённый код.
Что выведет код?
anonymous poll
start i0 start i1 start i2 – 5
👍👍👍👍👍👍👍 71%
start start start i0 i1 i2 – 1
👍 14%
Код упадёт с ошибкой StackOverflow – 1
👍 14%
i0 i1 i2 start start start
▫️ 0%
start i0 start i0 start i0
▫️ 0%
Код не скомпилируется
▫️ 0%
👥 7 people voted so far.
anonymous poll
start i0 start i1 start i2 – 5
👍👍👍👍👍👍👍 71%
start start start i0 i1 i2 – 1
👍 14%
Код упадёт с ошибкой StackOverflow – 1
👍 14%
i0 i1 i2 start start start
▫️ 0%
start i0 start i0 start i0
▫️ 0%
Код не скомпилируется
▫️ 0%
👥 7 people voted so far.
Правильный ответ на пятничный вопрос -
start start start i0 i1 i2
. Сам результат при этом не так интересен, как приводящая к нему логика исполнения программы. Чтобы её увидеть, можно добавить в код дополнительные отладочные выражения. Обратите внимание, что написать println("end")
в конце нельзя, так как тип результата блока reset
согласован с типом функции f
.val x = reset {Cоответствующий вывод в консоль:
def iterate(n: Int) = shift { f: (Int => Int) =>
println(s"start $n")
val x = f(0)
println(s"got back to $n - got res $x")
println(s"ran $x")
x + 1
}
println("pre-start 0")
iterate(0)
println("pre-start 1")
iterate(1)
println("pre-start 2")
val res = iterate(2)
println("end")
res
}
println(x)
pre-start 0
start 0
pre-start 1
start 1
pre-start 2
start 2
end
got back to 2 - got res 0
ran 0
got back to 1 - got res 1
ran 1
got back to 0 - got res 2
ran 2
3