Skip to content

Instantly share code, notes, and snippets.

@cryogenian
Last active June 14, 2023 12:44
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save cryogenian/f6df15df18a9d13e97f55d7fb547c910 to your computer and use it in GitHub Desktop.
Save cryogenian/f6df15df18a9d13e97f55d7fb547c910 to your computer and use it in GitHub Desktop.

DSl'и и как писать большие программы на PureScript'е

Вступление

Приветы! Меня зовут Максим Зималиев, я работаю в SlamData и уже почти два с половиной года пилю в продакшн код на PureScript'е. Как и большинство пришедших в прекрасный мир ФП, я начинал со всяких там джаваскриптов, пхп и шарпов. Соответственно, у меня есть бывшие коллеги и друзья, которые по-прежнему работают на императивных языках, используют GoF, DDD и тому подобное.

Ну так вот, пойдешь такой пиво пить, и внезапно "Слушай, а как у вас в PureScript'е, например ORM запилить, объектов же нет?" Или: "Это здорово всё, но вот я домены описываю вот так вот, у них там поведение есть, а как это PureScript'е сделать?" Или: "А что у вас вместо GoF?" И так далее... И тому подобное...

Так-то я отбиваюсь, дескать, не нужен нам ORM, не нужен DDD и GoF не нужен. Правда, почему-то мои "не нужно" не вызывают доверия :(

Короче говоря, я тут думал-думал и решил систематизировать ответ на все эти вопросы, хехей!

Типы, паттерны и грустный программист

Типы

Вообщем, представим себе, что нам нужно написать программку, которая управляет роботом

var Beer = function() {
};
var Rod = function() {
};
var Robot = function() {
};
Robot.prototype.bend = function(rod) {
  console.log("Bend!");
  return this;
};
Robot.prototype.drink = function(beer) {
  console.log("Drink!");
  return this;
};

И нам надо робота заправить, а потом согнуть арматурину.

var bender = new Robot();
var beer = new Beer();
var rod = new Rod()
bender.drink(rod); 
bender.bend(rod);

Понятное дело, что мы как правильные и хорошие программисты не просто так все это выложим в продакшн, а прогоним тесты, или хотя бы попросим кого-нибудь сделать ревью.

Эй, чел, твой робот пьет арматуру

Ой! Исправляем, отправляем снова, все отлично. Было бы неплохо, если бы нам не приходилось расстраивать нашего коллегу. И вот мы используем тесты, всякие instanceof ну или ультимативно переходим на TypeScript

class Rod {} 
class Beer {} 
class Robot {
  drink(beer: Beer): Robot {
    console.log("Drink!");
    return this;
  }
  bend(rod: Rod): Robot {
    console.log("Bend!");
    return this;
  }
}

Океюшки, теперь роботы не могут пить арматурины!

var bender = new Robto(); 
bender
  // .drink(new Rod()) won't compile
  .drink(new Beer())
  .bend(new Rod());

Наш коллега стал счастливее! Мы только что запретили себе и другим делать бессмысленные глупости.

Нужно больше роботов!!!

У нас есть робот, у робота есть пиво: время УБИТЬ ВСЕХ ЧЕЛОВЕКОВ!!!. Для этого нужно больше роботов.

class RobotFactory {
  robots: Robot[];
  make(): Robot {
    var robot = new Robot();
    this.robots.push(robot);
    return robot;
  }
  KILL_ALL_HUMANS(): void {
    if (robots.length > 1000000) {
      console.log("SUCCESS!");
    } else {
      console.log("There is not enough robots :(");
    }    
  }
}

Работать будет так

var factory = new RobotFactory();
for (var i = 0; i < 10000000; i++) {
  factory.make();
}
factory.KILL_ALL_HUMANS();

Отправляем на ревью. Грустный коллега

Слушай, у тебя тут класс с размытой областью ответственности, так делать нельзя. Нужно прикрутить какой нибудь другой класс для этого.

сlass RobotFactory {
  make(): Robot {
    return new Robot();
  }
}
class RobotArmy {
  var robots: Robot[];
  joinArmy(robot: Robot): RobotArmy {
    robots.push(robot);
    return this;
  }
  KILL_ALL_HUMANS(): void {
    if (robots.lenght > 1000000) {
      console.log("SUCCESS: all humans are killed!");
    } else {
      console.log("FAILURE: please add more robots to this army");
    }
  }
}

// client code 
var army = new Army();
var factory = new Factory();
var i: Number, robot: Robot;
for (i = 0; i < 10000000; i++) {
  robot = factory.make();
  army.joinArmy(robot);
}
army.KILL_ALL_HUMANS();

Ну ок, пойдет. Заметили общее? Суть в том, что мы сами отвергаем какие-то программы. Почему?

  • Если куски программ работают только с одной областью ответственности, то их проще понять.
  • Чем ниже когнитивная нагрузка, тем больше может быть приложение в целом!

Причем здесь паттерны? При том, что паттерны они, потому что они одинаковые. Все фабрики просто собирают: роботов-убийц, хтмл-элементы, пиво. Зная паттерн, программист может быстро понять, что код ему соответсвует или не соответствует. Короче говоря, они снижают сложность анализа и приема/отбрасывания кусков кода для человека!

Одинаковые фабрики строят одинаковых роботов

Еще разок

class Robot {
  bend(rod: Rod) { return this; }
  drink(beer: Beer) { return this; }
}
class Factory {
  make(): Robot { return new Robot(); }
}
// client
var factory = new Factory();
var beer = new Beer();
var rod = new Rod();
factory.make().drink(beer).bend(rod);

Внимательнее

factory
  .make()
  .drink(beer)
  .bend(rod)

Этот код

  • Подмножество TypeScript, нельзя сделать что-то вроде factory.make().drink(beer)[0]
  • У него есть значение drink(beer) очевидно значит пить пиво
  • Он работает с конкретной областью реальности. Предполагается, что роботы пьют пиво, знаете ли :)

А значит

  • У него есть синтаксис
  • У него есть семантика
  • У него есть домен Боже мой! Это же специальный доменный язык! Не может быть! Как так-то???

На самом деле, оказывается, что у всех этих ваших паттернов есть синтаксис/семантика/домен. Даже у метапаттернов, даже в PHP :)

Проблема у нас только одна, TS не может в проверку структруы DSL'ей :(

У меня будут свои паттерны! С блэкджеком и типами!

Лучшего способа для отбраковки "плохих" программ, чем типы на мой взгляд нет. Чем мощнее система типов, тем больше можно обраковать.

  • Нет типов: добро пожаловать undefined is not a function
  • есть типы: уже не напишешь foo = 1 + "2"
  • есть типы высших порядков: можно проверить проверить, что структура паттернов выполняется
  • есть зависимые типы: ХАХАХА! Ты больше не разделишь ничего на ноль!

Про зависимые типы посмотрите где-нибудь в другом месте, я сосредоточусь на PureScript'е и том, как в нем писать DSL'и.

Внимание! Внимание! DSL'и -- это способ организации программ и разделения ответственности, который понижает когнитивную сложность, и, как следствие позволяет управлять большим по размеру проектом. Паттерны -- унифицированный способ, постройки DSL'ей, унифицирован он, чтобы его было проще проверить человеку. Жителям ООП мира не интересны билдеры/фабрики/посетители/DDD, им интересны способы унификации интерфейсов доменных языков, чтобы их проще было проверить.

011001100

В PS есть такая фича -- тайпклассы. Она используется примерно так же, как интерфейсы или протоколы. Определяет контракт, короче говоря. Там, естественно, есть пара нюансов, вроде того, что для того, что у тайпклассов есть законы, которые должны выполняться, что это не про наследование, и никакого отношение к ООП классам они не имеют, хотя и наследуются

class Semigroupoid a where 
  append :: a -> a -> a 
class Semigroupoid a <= Monoid a where 
  mempty :: a 

-- client 
dummyExample :: forall a. Monoid a => Boolean -> a -> a 
dummyExample true a = a 
dummyExample false _ = mempty 

Так-то поглядите на dummyExample

  • Он использует подмножество языка: a + "foo" -- бессмыслица
  • У него есть значение, правда абстрактное: ноль и сложить
  • У него есть домен: штуки у которых есть ноль и можно их сложить.

Так что это DSL с двумя ключевыми словами: <> и mempty.

Тайпклассов очень много, все они определяют контракты и у них есть свои значения. Круто вот что: мы можем их использовать вместе. Тем самым расширяя наш "язык"

otherMeaninglessExample :: forall a. Monoid a => Show a => Eq a => а -> String
otherMeaninglessExample a 
  | a == mempty = "This is mempty"
  | otherwise = show a 

Впрочем ничего нового :| интерфейсы работают похоже.

100100100

Время поговорить о монадах. В общем если моноид -- это штука, у которой есть ноль и его можно сложить, то монада (я тут иерархию склеил)

class Monad m where 
  pure :: a -> m a 
  bind :: m a -> (a -> m b) -> m b -- он же >>=
  map :: (a -> b) -> m a -> m b
  apply :: m (a -> b) -> m a -> m b

Ничего я тут объяснять не буду. Просто приведу примерчик.

monadicComputation z = 
  takeFromContext >>= \x -> 
  workWithContext x >>= \y -> 
  pure (x + y * z) 

monadicDoComputation z= do 
  x <- takeFromContext 
  y <- workWithContext x 
  pure (x + y * z)

Эта штука очень-очень похожа на императивные вычисления, именно поэтому она так часто используется. В целом, монада с точки зрения доменных языков -- минимальная реализация императивного вычисления как цепочки. Ее еще и расширять можно, например MonadState s -- любое "императивное" вычисление в контексте состояния s. Это выглядит немножко странно, то есть, на кой черт нужен маленький язык для императивных вычислений? А нужен он, потому что императивные вычисления в фп было бы неплохо выделить и ограничить, (Так же как и работу с состоянием, или с моноидом, или с профунктором) потому что не все вокруг цепочки вычислений.

Внимание! Внимание! Монада -- это не цепочка вычислений! На самом деле монада -- это монада. Просто ее можно использовать в качестве DSL'я, который моделирует цепочку вычислений.

about:robots

Абстракции -- это очень абстрактно. Они уже позволяют нам выделять разные штуки, и (уверяю вас) писать код, в котором области ответственности отлично разделены. Но хотелось бы снова вернуться к роботу. Очевидно, что робот -- это не монада и не моноид, и даже не функтор. Так же как фабрика роботов -- это не полугруппа. Можно ли как-то использовать эти ваши тайпклассы с роботами?

Можно! Это даже (внезапно!) паттерн, который называется Finally Tagless. В нем мы моделируем поведение нашего маленького языка через тайпкласс, предельно конкретный тайпкласс.

class RobotProgram robot beer rod army factory | robot -> beer, robot -> rod, robot -> factory, robot -> army where 
  makeRobot :: factory -> robot 
  drink :: robot -> beer -> robot 
  bend :: robot -> rod -> Tuple rod robot 
  joinArmy :: army -> robot -> army 
  killAllHumans :: army -> Boolean

Внимание! Скорее всего эти вычисления затрагивают состояние и это должно быть указано, я опустил это для простоты. Функциональные зависимости -- штука, которая говорит, что пиво, например, полностью определяется роботом.

Использовать так

data Factory = Factory 
data Beer = Beer 
data Robot = Robot 
data Factory = Factory 
data Rod = Rod 

instance robot :: RobotProgram Robot Beer Rod (Array robot) Factory where 
  makeRobot _ = Robot 
  drink robot _ _ = robot 
  bend robot rod = Tuple robot rod 
  joinArmy army robot = cons robot army 
  killAllHumans a = length a > 1000000 

test 
  :: forall robot beer rod army factory
   . RobotProgram robot beer rod army factory 
  => Monoid army 
  => factory 
  => army 
test factory = 
  let 
    emptyArmy = monoid 
    robot = makeRobot factory 
  in joinArmy emptyArmy robot

Опять же, в этом примере, язык RobotProgram расширен (хотя, я предпочитаю, сужен, потому что не все подходящие армии моноиды, знаете ли) ограничением на тип армии, она может быть пустой. Мы могли бы засунуть это ограничение в определение RobotProgram, кстати говоря.

Вроде как вывод про тайпклассы.

  • Тайпклассы обеспечивают абстракцию
  • Определяют синтаксис
  • Обладают значением
  • Поддерживают связность
  • Код использующий их проще в понимании и поддержке.
  • Они проверяются компилятором.

Это к чему, это к тому, что они уже решают все, что нужно решать паттернам.

Bender local brewery

Классы типов -- это здорово, они позволяют решить нашу проблему. Однако, иногда такой подход бывает немного, эм... многословным. И у него, в случае finally tagless, есть недостаток, который на первый взгляд не особо виден.

class Robot robot beer rod | robot -> beer, robot -> rod where 
  bend :: robot -> rod -> Tuple robot rod 
  drink :: robot -> beer -> robot 

data Robot = Full | Empty
data Beer = Beer 
data Rod = Straight | Bended 

instance robot :: Robot Robot Beer Rod where 
  bend r _ = Tuple r Bended
  drink _ _ = Full

Теперь определим пивоварню

class Brewer brewery beer | brewery -> beer where
  brew :: brewery -> beer

data Brewery = Brewery 
instance brewery :: Brewery Brewery where
  brew _ = Beer

Теперь попробуем сделать робота-пивоварню и попробуем использовать существующие интерпретаторы

-- Erm... 
data BrewerRobot = BrewerRobot Robot Brewery 

instance robotBrewery :: Robot BrewerRobot Beer Rod where 
  bend (BrewerRobot r _) rod = bend r rod
  drink (BrewerRobot r _) beer = drink r beer

instance brewRobot :: Brewery BrewerRobot Beer where 
  brew (BrewerRobot _ b) = brew b 

То есть мы конечно использовали код повторно, но как-то это повторно не очень повторно. Прикол в том, что если у нас есть два интерпретатора/комплиятора для тайпклассового DSL'я, мы не можем их просто сложить. Нужно делать новый тип данных, нужно вручную диспатчить, что не может не напрягать.

Алгебраические што?

Так-то в PureScript'е есть еще несколько прекрасных абстракций, например, алгебраические типы данных. Те самые с конструкторами, сопоставлением с образцом и прочими радостями. Попробуем описать команды робота и пивоварни в виде ADT.

data RobotCommand beer rod
  = Drink beer
  | Bend rod 

data BreweryCommand beer
  = Brew beer

Отлично! Что теперь? Программа наша -- это же список команд, ну так давайте и запилим список(здесь массив будет) команд:

robotCommands :: Array (RobotCommand Robot Beer Rod)
robotCommands = 
  let 
    beer = Beer 
    robot = Robot 
    rod = Rod
  in [ Drink beer, Bend Rod ] 
  
breweryCommands :: Array (BreweryCommand Beer)
breweryCommands = [ Brew Beer, Brew Beer ]
  

Опять же, список команд -- штука чрезвычайно строгая, в ней могут быть только команды, типы же, все дела. Чтобы интерпретировать такую штуку можно использовать что-нибудь вроде этого:

interpretRobot :: forall e. Array (RobotCommand Robot Beer Rod) -> Eff (console :: CONSOLE|e) Unit
interpretRobot cs = for_ cs interpretRobotCommand 

interpretRobotCommand = case _ of 
  Drink _ -> log "Drink!"
  Bend _ -> log "Bend!" 

interpretBrewery :: forall e. Array (BreweryCommand Beer) -> Eff (console :: CONSOLE|e) Unit
interpretBrewery cs = for_ cs interpretBreweryCommand 

interpretBreweryCommand = case _ of 
  Brew _ -> log "Brew..."

Ну это-то понятно, а в чем профит?

type Command = Either (BreweryCommand Beer) (RobotCommand Robot Beer Rod)

program :: Array Command 
program = 
  let 
    robot = Robot 
    beer = Beer 
    rod = Rod 
  in [ Right $ Brew beer, Right $ Brew beer, Left $ Drink beer, Left $ Bend rod ] 

interpretCommand :: forall e. Array Command -> Eff (console :: CONSOLE|e) Unit
interpretCommand cs = for_ cs $ either interpretBreweryCommand interpretRobotCommand

Тададам!

  • Не нужен новый тип данных, его, конечно, можно добавить, но это необязательно.
  • Интерпретаторы работают так же, как и раньше. Их можно склеивать и так далее, и тому подобное.

Я вам вот что скажу...

DSL'и построенные на списках очень просты, если к ним прикрутить что-нибудь еще поинтереснее, то можно получить, например, HTML из purescript-halogen. Они работают быстро и вообще говоря очень даже понятны.

Но иногда хочется странного, например, использовать монады, просто так, для красоты. Особенно это украшательство прикольно работает, если у нас там вложеные команды. Сравните:

program = 
  [ SayHello
  , SubProgram 
    [ OpenPort
    , SendMessage
    , ClosePort
    ] 
  , Exit 0
  ] 

programDo = do 
  sayHello
  subProgram do 
    openPort 
    sendMessage
    closePort
  exit 0

Разницы особой нет, но мне лично больше нравится монадка тут. Чтобы это сделать на самом деле даже напрягаться не надо! Есть такая штука называется MonadTell и WriterT, первый -- это класс, который определяет синтаксис для императивных вычислений, который умеют писать в лог, второе -- это реализация этой штуки (конкретная то есть).

brew :: Beer -> WriterT (Array Beer) Unit
brew beer = tell [ beer ] 

brewery :: WriterT (Array Beer) Unit
brewery = do 
  brew Beer
  brew Beer
  brew Beer

interpret :: forall e. WriterT (Array Beer) Unit -> Eff (console :: CONSOLE|e) Unit
interpret = interpretBrewery $ runWriter brewery

Мне лично нравится.

Свободная касса!

Что если робот может сгибать только, если он заправлен? Это значит, что нам нужно как-то узнать состояние робота. То есть подъязык теперь не только список команд, он еще и возвращать что-то должен уметь. Естественно, что со списком этого не сделать. Нам нужна монада, и WriterT не подойдет. Может подойти State но о нем я не буду говорить, резко бросившись к свободным монадам.

Итак, чтобы сделать монаду нам надо уметь

  • Оборачивать значение вне монады в монаду pure
  • Связывать монадические вычисления >>=

И есть такая штука, которая умеет делать вот эти вот две операции для любого типа данных, который имеет дополнительный параметр.

Внимание! Внимание! Ковариантный параметр! data Foo a = Foo (a -> Int) не подойдет! Кроме того, в общем случае свободная монада работает с функторами, просто PureScript'овая библиотека на самом деле делает Freer monad, которые работают для алгебр в общем виде.

Штука эта -- свободная монада (Free Monad). И она позволяет описать что-то вроде

-- Мне надоело делать этот тип параметризованным :)
data RobotF a 
  = Bend Rod a 
  | Drink Beer a
  | IsEmpty (Boolean -> a)

type Robot = Free RobotF 

bend :: Rod -> Robot Unit
bend rod = liftF $ Bend rod unit 

drink :: Beer -> Robot Unit 
drink beer = liftF $ Bend beer unit 

isEmpty :: Robot Boolean
isEmpty = liftF $ IsEmpty id

program :: Robot Unit 
program = do 
  needBeer <- isEmpty 
  when needBeer $ drink Beer
  bend Rod

Чтобы эту штуку интерпретировать надо использовать foldFree, там параметр натуральная трансформация. Что-то вроде

interpretRobot :: forall e a. Robot a -> Eff (console :: CONSOLE|e) a
interpretRobot = foldFree nat 

robotNat :: forall e. RobotF ~> Eff (console :: CONSOLE|e) Unit
robotNat = case _ of 
  Drink beer next -> do 
    log "Drink!" 
    pure next
  Bend rod next -> do 
    log "Bend!"
    pure next
  IsEmpty cont -> do 
    log "I have no idea, because I'm dummy example implementation" 
    pure $ cont false 

Чтобы склеивать команды в свободной монаде надо использовать не Either, а Coproduct (тот же ейзер, но для штук с параметром типа)

data BreweryF a = Brew (Beer -> a) 

interpretBrewery :: forall a e. Free BreweryF a -> Eff (console :: CONSOLE|e) a
interpretBrewery = foldFree breweryNat

breweryNat :: forall e. BreweryF ~> Eff (console :: CONSOLE|e) a
breweryNat = case _ of 
  Brew cont -> pure $ cont Beer

brew :: Free BreweryF Beer 
brew = liftF $ BrewF id 
type BrewerRobotF = Coproduct BrewerF RobotF 

program = do 
  beer <- left brew
  needBeer <- right isEmpty 
  when needBeer $ drink beer -- Ура! Вечный двигатель!
  bend Rod

interpret = foldFree $ coproduct breweryNat robotNat

Точно такая же штука, как со списковым доменным языком, но у нас тут есть <- и это немножко увеличивает мощность языка. (Уже не говоря о том, что Free RobotF a -- аппликатив, функтор и так далее).

Вроде как вывод про свободные монады

  • Код по-прежнему расширяем, ограничен (очсильно) и выглядит прямо скажем неплохо
  • У языка есть четкая спецификация так же как в ТК
  • Интерпретация и сама программа разделены, как и в ТК.
  • Легко расширяемый синтаксис -- новые команды можно добавлять через копродукты.

Все это делает поддержку таких программ простой, а модульность просто необычайно высокой. По опыту могу сказать, что при работе со свободными монадами обычно даже не думаешь о том, что они где-то там интерпретируются.

Внимание! Внимание! Свободными бывают не только монады. Еще и аппликативы, моноиды, полукольца, да что угодно. И большая часть этих структур данных полезна! Потому что позволяет работать с заданной структурой (вот эта вот RobotF, например) абстрактно и независимо.

Мне нужна твоя одежда и мотоцикл

А еще свободные монады (аппликативы и прочие) умеют быть инстансами классов типов. И это тоже полезно! Потому что у нас есть жесткий абстрактный контракт (например, последовательность команд, которая может неожиданно завершиться ошибкой), которому удовлетворяет абстрактная структура данных (например, свободная монада).

class Monad robot <= RobotDSL robot where
  needBeer :: robot Boolean
  bend :: Rod -> robot Unit 
  drink :: Beer -> robot Unit 

program :: forall robot m. RobotDSL robot Beer Rod => MonadThrow String robot => robot Beer
program = do 
  isEmpty <- needBeer
  when isEmpty $ throw "Robot is empty, it can't bend"
  bend Rod

Тут опять finally tagless, ага. Монады свободные тут причем? Притом

newtype RobotM a = RobotDSL (Free RobotF a)

derive instance newtypeRobotDSL :: Newtype (RobotM a) _ 
derive newtype instance functorRobotDSL :: Functor RobotM
derive newtype instance applyRobotDSL :: Apply RobotM
derive newtype instance applicativeRobotDSL :: Applicative RobotM
derive newtype instance bindRobotDSL :: Bind RobotM
derive newtype instance monadRobotDSL :: Monad RobotM

instance robotMRobotDSL :: RobotDSL RobotM where 
  needBeer = RobotM <<< (liftF $ IsEmpty id)
  bend rod = RobotM $ Bend rod unit 
  drink beer = RobotM $ Drink beer unit

А вот теперь это уже серьезно.

  • Код клиента понятия не имеет, что это свободная монада. Он использует только ограничения классов типов.
  • Алгебры свободной монады по-прежнему можно складывать копродуктами.
  • Интерпретаторы по-прежнему можно менять в рантайме.

Shut up and take my money!!!

На самом деле сочетания ограничений тайпклассами и свободными штуками уже достаточно, чтобы делать гигантские вещи. Но, если вдруг.

Если я хочу использовать язык программирования в языке программирования, то я могу не ограничиваться тем, свободными монадами, списками команд, тайпклассами. Я могу просто запилить язык программирования! Хехей! Тем более, что это офигительно просто и очень похоже на работу со свободными монадами.

На самом деле Free это частный случай представления того, о чем сейчас пойдет речь. В общем случае это рекурсивные и корекурсивные штуки, которые умеют сворачивать и разворачивать алгебры (те штуки с дополнительными параметрами вроде RobotF). Здесь я использую purescript-matryoshka, потому что я к ней привык и она удобная.

Для того, чтобы сделать эту штуку надо

  • Определить базовую алгебру синтаксического дерева
data RobotC 
  = Bend Rod a 
  | CaseEmpty a a 
  | Drink Beer a

-- Эти инстансы нужны, чтобы можно было собирать, разбирать дерево потом
instance functorRobotC :: Functor RobotC where 
  map f = case _ of 
    Bend rod a -> Bend rod $ f a 
    CaseEmpty a b -> CaseEmpty (f a) (f b) 
    Drink beer a -> Drink beer $ f a 

instance foldableRobotC :: Foldable RobotC where 
  foldl f = ...
  foldr f = ...
  foldMap f = ...

instance traversableRobotC :: Traversable RobotC where 
  traverse = ...
  sequence = ... 
  • Собрать дерево используя embed (если мы дико крутые, то можем запилить парсер и свой прямо синтаксис, как у невстроенных языков)
program :: forall t. Corecursive t RobotC => t RobotC 
program = 
  embed 
  $ CaseEmpty 
      (embed $ Drink Beer $ embed $ Bend Rod) 
      (embed $ Bend Rod) 
  • Свернуть его используя алгебру!
alglog :: Algebra RobotC (Array String)
alglog = do 
  CaseEmpty a b -> cons "case" $ a <> b
  Drink _ a -> cons "drink" a 
  Bend _ a -> cons "bend" b

logAllCommands :: forall t. Recursive t RobotC => t RobotC -> Array String 
logAllCommands = cata alglog

Я не очень профессионал в таких делах, поэтому ограничусь тем, что скажу, что даже с бойлерплейтом

Конец

парам-парам-пам!

@gabriel-fallen
Copy link

s/new Robto/new Robot/

@gabriel-fallen
Copy link

s/обраковать/отбраковать/

@gabriel-fallen
Copy link

есть типы высших порядков: можно проверить проверить

Проверить проверку - это высший порядок или опечатка? 😆

@gabriel-fallen
Copy link

Паттерны -- унифицированный способ, постройки DSL'ей

Запятая после "способ" не нужна. Ну и после "DSL'ей" можно поставить точку и начать новое предложение.

@gabriel-fallen
Copy link

test 
  :: forall robot beer rod army factory
   . RobotProgram robot beer rod army factory 
  => Monoid army 
  => factory 
  => army 

А точно все стрелочки "жирные"?

    emptyArmy = monoid

Может, mempty?

@gabriel-fallen
Copy link

instance brewery :: Brewery Brewery where
Кажется, ещё нужен Beer.

@gabriel-fallen
Copy link

robotCommands = 
  let 
    beer = Beer 
    robot = Robot 
    rod = Rod
  in [ Drink beer, Bend Rod ] 

Должно быть, Bend rod? И неясно зачем robot = Robot?

@gabriel-fallen
Copy link

gabriel-fallen commented Jul 5, 2017

interpretRobot = foldFree nat

foldFree robotNat?

@gabriel-fallen
Copy link

when needBeer $ drink beer -- Ура! Вечный двигатель!

when же выполняется 0 либо 1 раз - откуда вечный? :)

@gabriel-fallen
Copy link

У языка есть четкая спецификация так же как в ТК

А что такое "ТК"?

@gabriel-fallen
Copy link

program :: forall robot m. RobotDSL robot Beer Rod

Зачем тут m, Beer и Rod?

@gabriel-fallen
Copy link

ограничусь тем, что скажу, что даже с бойлерплейтом

Кажется, ты слишком сильно ограничился... 😆

@vyorkin
Copy link

vyorkin commented Jan 5, 2018

🔥 спасибо! а есть/будет еще такое же? хотя я давно уверовал, учусь

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment