# Haskell: ДЗ 5 – Продвинутые типы и линзы
## Продвинутые типы
### Задание 1. ST (но не репер)
Реализуйте свой аналог `ST` монады, поддерживающий значения произвольного типа.
Код должен содержаться в отдельном модуле, модуль не должен экспортировать ничего что может позволить сломать инварианты ST монады.
Тип данных `STRef` должен соответствовать переменной, существующей в контексте Вашей `ST` монады.
В реализации можете использовать`Typeable`.
В отдельном модуле реализуйте примеры использования.
Данный пример должен компилироваться с Вашим кодом и вычислять i-е число Фибоначчи:
```haskell
fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib i = runST $ do
s <- newSTRef 0
t <- newSTRef 1
forM_ [2..i] $ \_ -> do
_s <- readSTRef s
_t <- readSTRef t
writeSTRef s _t
writeSTRef t (_s + _t)
readSTRef t
```
Следующий пример также должен компилироваться и вычислять квадратный корень числа с плавающей точкой.
```haskell
eps :: Double
eps = 1.0e-15
whileM :: Monad m => m Bool -> m () -> m ()
whileM c act =
c >>= \b ->
if b
then act >> whileM c act
else pure ()
sqrt' :: Double -> Double
sqrt' x
| x < 1 = error "x < 1 not supported"
| x == 0 = 0
| otherwise = runST $ do
l <- newSTRef 0
r <- newSTRef x
let checkCond = do
l_ <- readSTRef l
r_ <- readSTRef r
pure (r_ - l_ > eps)
whileM checkCond $ do
l_ <- readSTRef l -- l^2 < x
r_ <- readSTRef r -- r^2 >= x
let m = (l_ + r_) / 2
if (m * m >= x)
then writeSTRef r m
else writeSTRef l m
readSTRef r
```
### Задание 2. HalyavaScript
В данном задании от Вас потребуется реализовать новый язык программирования, который назовём HalyavaScript.
HalyavaScript -- статически типизируемый язык программирования, повторяющий основные конструкции языка JavaScript.
Реализовать HalyavaScript требуется в виде eDSL на Haskell, таким
образом получая type inference и parsing из коробки.
HalyavaScript поддерживает следующие типы:
* `Int32` (конвертируется в `Number`)
* `Double` (конвертируется в `Number`)
* `String`
* `Boolean`
* Функции от одного и двух агрументов
Конкретный синтаксис Вам предлагается разработать самим.
Для вдохновения можно использовать пример ниже:
```haskell
-- | For a given @x@ calculates @ceiling (log2 (a))@
log2 =
sFun1 $ \a logCnt ->
sWithVar 0 $ \accum ->
accum @= 1 #
logCnt @= 0 #
sWhile (a @> eRead accum)
( accum @= eRead accum + eRead accum #
logCnt @= eRead logCnt + 1
)
```
Обратите внимание, что у данной функции специально отсутвует сигнатура типа, чтобы Вы имели возможность придумать все типы сами.
Для реализации можете использовать GADT, tagless final, Free монады и другие продвинутые подходы языка.
Реализуйте интерпретатор Вашего языка, который будет интерпретировать код написанный на HalyavaScript.
К реализации должен быть приложен простой пример программы, в котором задействованы типы `Int` и `String`.
### Задание 3. HalyavaScript to JavaScript
Реализуйте еще один интерпретатор, который будет конвертировать код на HalyavaScript в строковое представление.
Например, код из предыдущего задания должен выглядеть как-то так. Необязательно точь-в-точь так же.
```javascript
function(v0){
var v1=0;
var v2=0;
v2=1;
v1=0;
while ((v0) > (v2)) {
v2=(v2) + (v2);
v1=(v1) + (1);
}
return v1
}
```
Напишите несколько unit-тестов, которые конвертируют код в строку и сравнивают ее с ожидаемым результатом.
### Задание 4. Погружение в HalyavaScript
Реализуйте 3 математические функции на HalyavaScript. Они должны использовать как `Int`, так и `Double`.
Математические функции можете писать какие хотите, но плюсом будет если они будут отличны от стандартных примеров вроде
факториала и чисел Фибоначчи.
Для каждой функции реализуйте QuickCheck тест, который тестирует соответствие результатов интерпретации программ на HalyavaScript выводу канонических реализаций тех же математических функций на стандартном хаскеле.
## Линзы
Данный блок заданий посвящён линзам. В начале блока предлагается реализовать базовые операции по работе с линзами, чтобы лучше понимать, как устроена библиотека. А затем идут задания на использование линз, чтобы научиться пользоваться этим мощным инструментом.
### Задание 5: Базовые линзы
В этом задании запрещается использовать библиотеки с линзами.
Настоящие линзы имеют следующий тип:
```haskell=
type Lens s t a b = forall f . Functor f => (a -> f b) -> s -> f t
```
Но для простоты мы будем использовать линзы Ван Лаарховена (или же «простые линзы»):
```haskell=
type Lens' s a = Lens s s a a
```
Реализуйте следующие базовые примитивы по работе с линзами:
```haskell
set :: Lens' s a -> a -> s -> s -- set value (setter)
view :: Lens' s a -> s -> a -- lookup value (getter)
over :: Lens' s a -> (a -> a) -> s -> s -- change value (modifier)
```
**_Подсказка_:** используйте функторы `Const` и `Identity` для этого задания.
Можно использовать операторные формы этих функций:
```haskell
(.~) :: Lens' s a -> a -> s -> s
(^.) :: s -> Lens' s a -> a
(%~) :: Lens' s a -> (a -> a) -> s -> s
```
После этого реализуйте простейшие линзы для пары (!!! реализуйте эти функции вручную, запрещается использовать `lens` из усложнённой версии !!!):
```haskell=
-- _1 :: Functor f => (a -> f b) -> (a, x) -> f (b, x)
_1 :: Lens (a, x) (b, x) a b
_1 = _
-- _2 :: Functor f => (a -> f b) -> (x, a) -> f (x, b)
_2 :: Lens (x, a) (x, b) a b
_2 = _
```
Напишите юнит-тесты, которые проверяют, что ваши линзы `_1` и `_2` работают как ожидается вместе с функциями `set`, `view`, `over`.
Линза представляет собой по сути пару из геттера и сеттера. Следовательно, прежде чем использовать линзу, необходимо научиться создавать её. Для этого реализуйте следующую функцию:
```haskell
lens :: (s -> a) -> (s -> b -> t) -> Lens s t a b
lens get set = _
```
**Подсказка:** Если реализовать функцию выше тяжело, то попробуйте для начала реализовать более простую версию.
```haskell=
lens :: (s -> a) -> (s -> a -> s) -> Lens' s a
│ │
│ └─ setter
│
└── getter
```
После этого реализуйте линзы, которые сложней, чем `_1` и `_2`:
```haskell
-- Объединить две линзы в одну, которая работает с Either.
choosing :: Lens s1 t1 a b
-> Lens s2 t2 a b
-> Lens (Either s1 s2) (Either t1 t2) a b
choosing l1 l2 = _
-- Изменить цель линзы и вернуть новый результат. Постарайтесь
-- реализовать без анонимных функций и определений своих функций
(<%~) :: Lens s t a b -> (a -> b) -> s -> (b, t)
(<%~) l f s = _
-- Изменить цель линзы, но вернуть старый результат.
(<<%~) :: Lens s t a b -> (a -> b) -> s -> (a, t)
(<<%~) l f s = _
```
### Задание 6: FileSystem lenses
Начиная с этого задания разрешается использовать пакет `microlens`.
Задан тип данных, которым можно представить простое дерево файловой системы:
```haskell
data FS
= Dir
{ name :: FilePath -- название папки, не полный путь
, contents :: [FS]
}
| File
{ name :: FilePath -- название файла, не полный путь
}
```
Создайте функцию, которая «сканирует» заданную директорию и создаёт объект типа `FS` наподобие функции [`getDirectory'`](https://hackage.haskell.org/package/filesystem-trees-0.1.0.6/docs/System-File-Tree.html#v:getDirectory-39-).
После этого создайте базовые линзы и призмы Вашего типа данных.
### Практика на линзы
> Блок практики не оценивается баллами. Он лишь предоставляет набор команд, которые полезно научиться реализовывать в одну строку при помощи линз, прежде чем приступать к дальнейшим заданиям. Все команды в списке очень простые, но если Вы не можете выполнить и их, значит, Вы не до конца разобрались с линзами, и выполнять дальнейшие задания ещё рано.
1. Список поддеревьев папки для `Dir`, иначе пустой список.
2. `Maybe` с именем директории, если `Dir`, или `Nothing` иначе.
3. Имя файла, если `File`, или пустую строку иначе.
4. Изменить имя корня дерева на `/`.
5. Добавить произвольный суффикс к имени корня дерева.
6. Получить имя самой первой папки в списке поддиректорий (`Nothing`, если такой нет).
7. Получить список имён только `File` из `Dir` (нерекурсивно).
### Задание 7: Обходы FS
Реализуйте следующие `Lens` или `Traversal`:
1. `cd`: перейти в поддиректорию с указанным именем.
2. `ls`: получить список имён содержимого директории.
3. `file`: получить имя конкретного `File`, если он существует.
В итоге должна быть возможность делать нечто похожее:
```haskell
myDir ^? cd "A" . cd "B" . file "C" -- Just "C" при существовании myDir/A/B/C
myDir ^.. cd "A" . cd "B" . ls -- получить содержимое myDir/A/B/
```
### Задание 8: Изменения структуры FS
#### Базовая версия
Реализуйте следующие функции, используя линзы:
1. Изменить в директории расширения всех файлов на другое (нерекурсивно).
2. Получить имена всех файлов и директорий рекурсивно.
3. Удалить выбранную поддиректорию, только если она пустая.
#### Усложнённое задание
4. Получить полный путь к файлу с названием файла относительно стартового узла `FS`.
Интерфейс должен быть следующим:
```haskell=
ghci> myDir ^? move "A" . move "B" . getPath -- myDir is labeled by 'root'
Just "root/A/B/"
```
Разумеется, что все действия происходят над Вашей структурой данных, а в вашей файловой системе ничего не меняется.