Posts
Здравствуй, дорогой дневник. Что-то давно я в тебя ничего не писал.
Уважаемый
esil0x подбил меня переписать мой "игрушечный веб" таким образом, чтобы
- Хранить всё состояние виджетов на сервере
- Не требовать возможности сериализовать состояние
- Сделать более гибкие виджеты - например, упоминавшийся мной "хамелеон", которые может вести себя как любой виджет, который нам будет угодно ему скормить в рантайме
Я, естественно, повёлся на это "слабо" и около недели писал всё это дело, прерываясь на текущую работу, ужин и убийство зомби. Представляю интересующимся результаты.
Для начала: полностью отказаться от сериализации, естественно, невозможно - да и не нужно. Всё-таки конечный продукт нашей деятельности - веб-страница, которая как сама должна передаваться по сети, так и действия пользователя должны передаваться обратно. Их-то (в смысле, действия пользователя) мы и будем сериализовать.
Как и раньше, мы будем считать, что каждый виджет имеет некий "входной" канал, по которому ему подаются команды, и "выходной" канал, в который он выдаёт результаты. Мы собираем виджеты воедино, используя некие комбинаторы - в основном, стрелочные. Кроме этого, мы будем считать, что каждый виджет имеет некий "выхлоп", который показывает, что именно сделал пользователь. Наконец, разумным кажется предположение, что результат, выдаваемый виджетом, полностью определяется его входом и его "выхлопом". В принципе, это не всегда так - например, возможен виджет, выход которого зависит от того, сколько времени прошло с момента загрузки страницы, или вообще случаен. Мы, однако, строим упрощённую модель и такие тонкости игнорируем.
Теперь далее: что вообще может сделать пользователь? В нашей модели он, фактически, может сделать только одно: щёлкнуть по какой-то ссылке. Если расширять модель - скажем, добавить в неё заполняемые пользователем формы - то добавится ещё вариант, например, "отправить форму с такими-то данными" (да-да, в этом игрушечном вебе нет форм). В любом случае, если на странице расположены несколько виджетов, пользователь может что-то сделать лишь с одним из них; "выхлоп" остальных должен показывать, что никаких действий не произведено.
Я думал вначале сделать "выхлоп" значением типа Maybe a, с тем, чтобы значение Nothing означало, что пользователь с этим виджетом ничего не делал. Однако, некоторые виджеты не содержат никаких ссылок, причём при комбинаторном подходе, когда сложные виджеты собираются из простых, виджетов без ссылок будет очень много. Можно бы, конечно, включить прагму EmptyDataDecls и использовать пустой тип для обозначения действий пользователя с таким виджетом, но при сериализации в результате получается куча лишнего текста. Я решил использовать более хитрый приём. Вместо Maybe a мы используем Either a () - это ведь то же самое. Для виджетов, не подразумевающих взаимодействие с пользователем, мы используем тип, изоморфный (). В любом случае, получается, что "выхлоп" виджета получается применением некоторого функтора к типу (). Далее, "выхлоп" пары виджетов получается как применение к тому же типу композиции соответствующих функторов. Всё это выражается в следующем модуле:
> module SerializeFunctor (SerializeFunctor, unit, Serialize, Id, Comp, writeSer, readSer, embedSer, mergeSer, fstSer, sndSer) where
> import Control.Monad.State
Не всякий функтор, разумеется подойдёт. Мы заводим специальный класс:
> class Functor f => SerializeFunctor f where
Нам понадобятся функции сериализации и десериализации. При этом мы будем предполагать, что сериализовать (соответственно, десериализовать) то, что находится внутри функтора, мы уже умеем.
> serialize :: (a -> State String ()) -> f a -> State String ()
> deserialize :: State String a -> State String (f a)
Кроме того, нам нужны будут функции, позволяющие из двух функторов составить один, и, соответственно, разобрать обратно - для того, чтобы делать составной виджет из двух в него входящих. Для этого предназначены следующие два метода - как именно они используются, будет ясно потом.
> unit :: f ()
> transform :: f a -> a -> a
Немного поясню. Предполагается, что метод
transform должен просто извлекать значение из-под функтора; если же значение извлечь нельзя - используется значение по умолчанию, переданное вторым аргументом.Сразу заведём функции сериализации и десериализации для случая, когда в нашем функторе зашит тип () - именно эти функции мы будем экспортировать.
> writeSer :: SerializeFunctor f => f () -> String
> writeSer fu = execState (serialize (\() -> return ()) fu) ""
> readSer :: SerializeFunctor f => String -> f ()
> readSer = evalState $ deserialize $ return ()
Пригодится также функция, позволяющая засунуть внутрь функтора любое значение:
> embedSer :: SerializeFunctor f => a -> f a
> embedSer x = fmap (\() -> x) unit
Далее, начнём заводить инстансы. Для начала - тот самый функтор, который даёт тип, изоморфный содержащемуся внутри; напоминаю, он будет использоваться для виджетов, с которыми пользователь взаимодействовать не может.
> newtype Id a = Id {fromId :: a}
> makeId :: a -> Id a
> makeId = Id
Функцию
makeId я завёл только по давно сформировавшейся привычке не экспортировать из модуля конструкторы - иначе изменишь тип данных на более общий, после чего рыскаешь по всем файлам проекта, ища, где ещё сломается.Как я и сказал, этот тип будет инстансом нашего класса:
instance Functor Id where fmap h (Id a) = Id (h a)
> instance SerializeFunctor Id where
> serialize ser (Id a) = ser a
> deserialize = liftM Id
> unit = Id ()
> transform (Id x) = const x
Далее, обещаный тип
Either. Я хочу иметь некоторый контроль над тем, что именно может быть "выхлопом", поэтому завожу вспомогательный класс без методов
> class (Read t, Show t) => Serialize t
и единственный инстанс, который мне здесь понадобится.
> instance Serialize ()
Если бы у меня были формы, я завёл бы инстансов побольше; сейчас же, когда все действия пользователя исчерпываются кликами (одной кнопкой) хватит и этого.
Обещанный инстанс:
> instance Serialize t => SerializeFunctor (Either t) where
> serialize _ (Left t) = modify (++ "L" ++ show t ++ "|")
> serialize ser (Right x) = modify (++ "R") >> ser x
> deserialize des =
> do c <- gets head
> modify tail
> case c of
> 'L' ->
> do value <- gets $ takeWhile (/= '|')
> modify $ tail . dropWhile (/= '|')
> return $ Left $ read value
> 'R' -> liftM Right des
> unit = Right ()
> transform (Left _) = id
> transform (Right x) = const x
Здесь, мне кажется, всё должно быть понятно. Если у нас левое значение - пишем букву "L" и само значение, добавляя в конец ограничитель "|" - вообще-то, он нам не очень нужен, но в будущем, если "фреймворк" будет развиваться, может пригодиться. Если у нас правое значение - пишем "R" и само значение; ограничитель не нужен, его добавит функция сериализации этого правого значения. При десериализации - идём обратно. Остальные методы - в общем-то, там у нас почти единственное, что может быть.
Наконец, композиция. Для начала, заведём сам тип.
> newtype Comp f2 f1 a = Comp (f2 (f1 a))
Сразу напишем функции, собирающие композицию или разбирающие обратно - вот тут нам и пригодятся методы
unit и transform.
> mergeSer :: Functor f2 => f1 () -> f2 () -> Comp f2 f1 ()
> mergeSer f1u f2u = Comp $ fmap (\() -> f1u) f2u
> fstSer :: (SerializeFunctor f1, SerializeFunctor f2) => Comp f2 f1 () -> f1 ()
> fstSer (Comp ffu) = transform ffu unit
> sndSer :: Functor f2 => Comp f2 f1 () -> f2 ()
> sndSer (Comp ffu) = fmap (const ()) ffu
И, соответственно, инстансы:
> instance (Functor f1, Functor f2) => Functor (Comp f2 f1) where fmap h (Comp ffa) = Comp (fmap (fmap h) ffa)
> instance (SerializeFunctor f1, SerializeFunctor f2) => SerializeFunctor (Comp f2 f1) where
> serialize ser (Comp ffa) = serialize (serialize ser) ffa
> deserialize des = liftM Comp $ deserialize $ deserialize des
> unit = Comp $ embedSer unit
> transform (Comp ffa) x = transform (transform ffa $ embedSer x) x
Ура, с модулем сериализации покончено.
Следующим пунктом нашей программы будет преобразователь стрелок, который из произвольной стрелки сделает новую, имеющую "выхлоп", причём, как я уже говорил ранее, вывод стрелки будет определяться тем, что пришло на вход, и "выхлопом".
> {-# LANGUAGE Arrows, ExistentialQuantification #-}
> module Emitter (Emitter, runEmitter, askEmitter, noEmission, emit) where
Мы будем делать нашу новую стрелку, в частности, инстансом класса
Category, для чего нам потребуется модуль Control.Category. Он, среди прочего, экспортирует функции id и (.), которые конфликтуют с аналогичными функциями в модуле Prelude. К счастью, они не просто так имеют те же имена - они обобщают функции Prelude. Поэтому, достаточно будет скрыть оригинальные, необобщённые версии:
> import Prelude hiding (id, (.))
Далее, пара импортов, стандартных для любого модуля, работающего со стрелками.
> import Control.Arrow
> import Control.Category
И, наконец, импортируем предыдущий модуль, отвечающий за сериализацию.
> import SerializeFunctor
Теперь собственно преобразователь стрелок:
> data Emitter a input output = forall emission. SerializeFunctor emission => Emitter (a input (emission ())) (input -> emission () -> output)
Как я и обещал, вместо стрелки из входа в выход мы получаем стрелку из входа в "выхлоп" и функцию, восстанавливающую выход. Так как тип "выхлопа" не фиксирован, мы убираем его под экзистеншиал.
Мы не будем работать с "выхлопом" напрямую - мы будем везде использовать его сериализованный вид. Поэтому, мы заведём пару вспомогательных функций, которые будут делать (де-)сериализацию за нас.
> runEmitter :: Arrow a => Emitter a input output -> a input String
> runEmitter (Emitter a _) = a >>^ writeSer
> askEmitter :: Emitter a input output -> input -> String -> output
> askEmitter (Emitter _ f) input str = f input $ readSer str
Вопрос: а почему вместо засовывания типа под экзистеншиал не работать сразу со строками? Ответ: в принципе, можно, но тогда соединение нескольких таких стрелок в одну будет нетипизированным кошмаром - работать можно, но очень неудобно.
Нам также понадобится удобный способ конструировать такие преобразованные стрелки. Для начала, нам пригодится "умный" конструктор, преобразующий произвольную (ну, не совсем произвольную) стрелку так, чтобы новая стрелка не выдавала никакого выхлопа:
> noEmission :: Arrow a => a input () -> Emitter a input ()
> noEmission a = Emitter (a >>^ makeId) (const fromId)
В "выхлопе" пойдёт тип
Id () - что, собственно, и означет "никакой информации".Кроме того, пригодится "умный" конструктор, позволяющий выдать такой "выхлоп", какой нам захочется:
> emit :: (Arrow a, Serialize t) => a input (Maybe t) -> Emitter a input (Either t ())
> emit a = Emitter (a >>^ maybe (Right ()) Left) (\_ -> id)
Наконец, нужно, чтобы наши "преобразованные" стрелки оставались стрелками. Здесь все инстансы пишутся практически единственным образом:
> instance Arrow a => Category (Emitter a) where
> id = arr id
> Emitter a2 e2 . Emitter a1 e1 = Emitter a e
> where a =
> proc input ->
> do emission1 <- a1 -< input
> emission2 <- a2 -< e1 input emission1
> returnA -< mergeSer emission1 emission2
> e input emission =
> let middle = e1 input $ fstSer emission
> in e2 middle $ sndSer emission
> instance Arrow a => Arrow (Emitter a) where
> arr f = Emitter (proc input -> returnA -< makeId ()) (\input _ -> f input)
> first (Emitter a e) = Emitter (proc (input, _) -> a -< input) (\(input, z) emission -> (e input emission, z))
> instance ArrowChoice a => ArrowChoice (Emitter a) where
> left (Emitter a e) = Emitter a' e'
> where a' =
> proc inputOrZ ->
> case inputOrZ of
> Left input -> a -< input
> Right z -> returnA -< unit
> e' (Left input) emission = Left $ e input emission
> e' (Right z) _ = Right z
> instance ArrowLoop a => ArrowLoop (Emitter a) where
> loop (Emitter a e) = Emitter a' e'
> where a' =
> proc input ->
> do rec {emission <- a -< (input, z);
> let (_, z) = e (input, z) emission;}
> returnA -< emission
> e' input emission = let (output, z) = e (input, z) emission in output
С "выхлопом" разобрались.
Чтобы два раза не вставать, заодно определим трансформер стрелок, позволяющий стрелке хранить некое состояние.
> {-# LANGUAGE Arrows #-}
> module Sequencer (Sequencer, runSeq, constSeq, state, chameleon) where
> import Prelude hiding (id, (.))
> import Control.Arrow
> import Control.Category
> import Data.Maybe
У нас уже был такой трансформер ранее. Он был таким:
data NetState a input output = forall local. (Serialize local, Pointed local) => NetState (a (input, local) (output, local))
Раз мы готовы хранить состояние на сервере (и его, стало быть, не нужно сериализовать), ограничение
Serialize local исчезает. При рефакторинге оказалось, что начальное состояние виджета не обязательно подавать во входном канале стрелки - что привело к следующему трансформеру:
data Keeper a input output = forall local. Pointed local => local -> Keeper (a input (output, local))
А затем я подумал: ведь единственная функция, в которую будет подставляться состояние виджета - вот эта; что нам мешает заранее произвести подстановку и исключить состояние вообще? Коль скоро мы не собираемся никуда его передавать - нам ничто не мешает так сделать. В результате, получается следующее:
> newtype Sequencer a input output = Sequencer {runSeq :: a input (output, Sequencer a input output)}
Парочка "умных" конструкторов. Во-первых, стрелка, которая не хранит никакого состояния:
> constSeq :: Arrow a => a input output -> Sequencer a input output
> constSeq a = let c = Sequencer {runSeq = a >>^ \output -> (output, c)} in c
Кроме того, нам понадобится стрелка, которая только хранит состояние (на этот раз - произвольного типа) и ничего больше не делает.
> state :: Arrow a => local -> Sequencer a (local -> local) local
> state local = Sequencer {runSeq = s} where
> s = proc localTransform -> returnA -< let local' = localTransform local in (local', state local')
Здесь аргумент функции - это начальное состояние стрелки.
Снова объявим наши "преобразованные" стрелки стрелками; опять все инстансы пишутся практически без каких-либо вариантов: каждый раз мы берём выходное значение из "вложенной" стрелки.
> instance Arrow a => Category (Sequencer a) where
> id = arr id
> s2 . s1 = Sequencer {runSeq = s}
> where s =
> proc input ->
> do (middle, s1') <- runSeq s1 -< input
> (output, s2') <- runSeq s2 -< middle
> returnA -< (output, s2' . s1')
> instance Arrow a => Arrow (Sequencer a) where
> arr f = Sequencer {runSeq = arr $ \input -> (f input, arr f)}
> first s = Sequencer {runSeq = s'}
> where s' =
> proc (input, z) ->
> do (output, sq) <- runSeq s -< input
> returnA -< ((output, z), first sq)
> instance ArrowChoice a => ArrowChoice (Sequencer a) where
> left s = Sequencer {runSeq = s'}
> where s' =
> proc inputOrZ ->
> case inputOrZ of
> Left input ->
> do (output, sq) <- runSeq s -< input
> returnA -< (Left output, left sq)
> Right z -> returnA -< (Right z, left s)
> instance ArrowLoop a => ArrowLoop (Sequencer a) where
> loop s = Sequencer {runSeq = s'}
> where s' =
> proc input ->
> do rec {((output, z), sq) <- runSeq s -< (input, z)}
> returnA -< (output, loop sq)
И, наконец, кусочек того, что просили: виджет-хамелеон. Он будет принимать на входе один "обычный" параметр, а другой, возможно, будет содержать следующий виджет - который заменит то, что идёт на выход:
> chameleon :: ArrowChoice a => Sequencer a input output -> Sequencer a (input, Maybe (Sequencer a input output)) output
> chameleon s = Sequencer {runSeq = s'} where
> s' =
> proc (input, mSeq) ->
> do (output, sq) <- runSeq s -< input
> returnA -< (output, chameleon $ fromMaybe sq mSeq)
Займёмся теперь тем, что происходит на нижнем уровне. Практически, мы это уже делали - в одной из предыдущих серий.
Для начала - маленький служебный модуль, который, по уму, должен бы быть в стандартной библиотеке, рядом с классом MonadPlus:
> module Additive(Additive(..)) where
> class Functor f => Additive f where
> azero :: f a
> aplus :: f a -> f a -> f a
Единственное, чем этот класс отличается от
MonadPlus, так это тем, что он унаследован не от Monad, а от Functor.Вводили мы этот класс не зря. Сейчас мы предьявим один его инстанс:
> module Render(Render, render, staticText, makeLink) where
> import Data.Monoid
Импортируем предыдущий модуль:
> import Additive
И, собственно, наш основной (и, по совместительству единственный) аддитивный функтор:
> newtype Render link html x = Render {render :: (x -> link) -> html}
В общем, он должен быть знаком тем, кто читал предыдущие серии. Он говорит о том, как надо отобразить некий тип данных; при этом единственное, что ему требуется - это указание, что писать в ссылках.
Соответственно, сразу вводим пару умных конструкторов. Первый позволяет отобразить текст без всяких ссылок:
> staticText :: html -> Render link html x
> staticText html = Render {render = const html}
Второй - наоборот, отображает только ссылку; при этом на вопрос "как из ссылки сделать HTML" предполагается ответить позднее.
> makeLink :: (link -> html) -> x -> Render link html x
> makeLink display x = Render {render = \linkMaker -> display $ linkMaker x}
Наконец, если мы умеем соединять куски HTML (а я думаю, мы умеем), то мы умеем и складывать эти рендеры - что позволит нам, скажем, сначала отрендерить одну часть, а потом другую.
> instance Functor (Render link html) where fmap f r = Render {render = \h -> render r $ h . f}
> instance Monoid html => Additive (Render link html) where
> azero = Render {render = const mempty}
> r1 `aplus` r2 = Render {render = \linkMaker -> render r1 linkMaker `mappend` render r2 linkMaker}
Ещё один модуль, отвечающий как раз за передачу сигналов от одного виджета к другому. Как оказалось, это вполне можно сделать не стрелкой, а монадой.
> module Signal(Signal, makeSignal, pure, effect) where
> import Control.Monad.Fix
> import Additive
Заметьте, что мы не импортируем модуль
Render - нам сейчас неважно, какой аддитивный функтор будет отвечать за отображение виджета, нас интересует только передача сигналов.
> data Signal f output = Signal {pure :: output, effect :: f output}
> makeSignal :: output -> f output -> Signal f output
> makeSignal output foutput = Signal {pure = output, effect = foutput}
Я снова предпочитаю не экспортировать конструктор.
Ну, собственно, монада. Метод return не отображает ничего вообще. Метод (>>=) отображает сначала один виджет, а потом другой:
> instance Additive f => Monad (Signal f) where
> return output = Signal {pure = output, effect = azero}
> s >>= h = let s' = h $ pure s in Signal {pure = pure s', effect = fmap (pure . h) (effect s) `aplus` effect s'}
> instance Additive f => MonadFix (Signal f) where mfix h = h $ fix $ pure . h
А теперь - собираем всё воедино.
> module Html where
Импортируем всё, что надо.
> import Control.Arrow
> import Control.Monad
> import Emitter
> import Render
> import Sequencer
> import Signal
Скажем, какой именно тип будут иметь ссылки и весь HTML.
> type Link = String
> type Html = [String]
Может быть, это и не лучший выбор с точки зрения производительности, но мы делаем прототип, даже игрушку, а не промышленный фреймворк.
Сразу заведём функции для вывода ссылок и статического текста:
> display :: String -> Link -> Html
> display caption link = [caption ++ " <" ++ link ++ ">"]
> oneString :: String -> Html
> oneString s = [s]
Наконец, то, о необходимости чего так долго твердили большевики. Собственно виджеты:
> type Widget = Sequencer (Emitter (Kleisli (Signal (Render Link Html))))
Много всего, да.
Парочка функций. Одна отображает текущий виджет:
> runWidget :: Widget () output -> Html
> runWidget w = render (effect $ runKleisli (runEmitter $ runSeq w) ()) id
Другая - по виджету и его "выхлопу" выдаёт нам виджет, который нужно будет отобразить на следующем шаге:
> nextWidget :: Widget () output -> String -> Widget () output
> nextWidget w str = snd $ askEmitter (runSeq w) () str
Отдельной функции, которая по виджету и его "выхлопу" возвращает его текущий вывод, не будет. Не потому, что её нельзя написать; просто она нафиг никому не нужна.
Соорудим виджет, отображающий статический текст, без какого-либо взаимодействия с пользователем:
> label :: Widget String ()
> label = constSeq $ noEmission $ Kleisli $ \text -> makeSignal () $ staticText $ oneString text
Константа. Никакого выхлопа. На выходе - всегда
(). Что может быть проще?Для второго виджета - ссылки - нам понадобится вспомогательная функция, устанавливающая изоморфизм двух простеньких типов, каждый из которых имеет ровно два значения:
> isLeft :: Either () () -> Bool
> isLeft = either (\() -> True) (\() -> False)
Думаю, понять, что эта функция делает, проще по её типу, чем по реализации.
Собственно, виджет-ссылка:
> link :: String -> Widget () Bool
> link caption = constSeq $ emit (Kleisli $ \() -> makeSignal Nothing $ makeLink (display caption) (Just ())) >>^ isLeft
Снова константа, но на сей раз - с выхлопом.
Это - почти всё. Ещё нам нужен простенький сервер, который будет брать виджет и рисовать его на экране. После этого мы каждый раз будем вводить ему номер шага и URL из ссылки, эмулируя таким образом мышеклик по ней. Номер шага нужен для эмуляции того, что пользователь открыл страницу в новом окне и работает там, а потом вернулся к старому окну. Поэтому вместо одного виджета мы будем хранить список - опять же, сомнительно с точки зрения производительности, но вполне достаточно для игрушки.
> server :: Widget () output -> IO ()
> server w = serverLoop [] w
> where
> serverLoop widgetList newWidget =
> do let newWidgetList = widgetList ++ [newWidget]
> putStrLn $ "*** " ++ show (length widgetList) ++ " ***"
> putStr $ unlines $ runWidget newWidget
> putStr ">>> "
> request <- getLine
> when (not $ null request) $
> let [reqIndex, reqEmission] = words request
> reqWidget = newWidgetList !! read reqIndex
> in serverLoop newWidgetList $ nextWidget reqWidget reqEmission
Что здесь есть? А почти ничего. Выводим номер очередного шага. Рендерим страницу. Выводим приглашение (слямзенное из питоновского REPL-а). Получаем запрос. Проводим простейший парсинг - если запрос пустой, не делаем ничего, если непустой - берём из него номер шага и "выхлоп" (который и пойдёт в URL ссылки) и начинаем заново. Всё.
Как ни странно, всё работает. Чтобы убедиться в этом, запустим несколько тестов.
> {-# LANGUAGE Arrows #-}
> module Tests where
> import Control.Arrow
> import Sequencer
> import Html
Первые пять тестов хорошо знакомы из предыдущих серий, последние два - новые.
Первый тест - ссылка, увеличивающая счётчик, текстовое поле, показывающее счётчик, и ссылка, обновляющая страницу.
> test1 =
> proc () ->
> do clicked <- link "+" -< ()
> number <- state (0 :: Integer) -< if clicked then (+ 1) else id
> label -< show number
> link "refresh" -< ()
Проверяем:
*Tests> server test1
*** 0 ***
+ <RL()|>
0
refresh <L()|>
щёлкаем по первой ссылке
>>> 0 RL()|
*** 1 ***
+ <RL()|>
1
refresh <L()|>
теперь - по второй
>>> 1 L()|
*** 2 ***
+ <RL()|>
1
refresh <L()|>
теперь - пару раз по первой
>>> 2 RL()|
*** 3 ***
+ <RL()|>
2
refresh <L()|>
>>> 3 RL()|
*** 4 ***
+ <RL()|>
3
refresh <L()|>
возвращаемся к одному из ранних шагов и проходим по другой ссылке
>>> 1 RL()|
*** 5 ***
+ <RL()|>
2
refresh <L()|>
завершаем тест
>>>
Теперь второй тест - две ссылки, одна из которых увеличивает счётчки, а другая - уменьшает:
> test2 =
> proc () ->
> do increase <- link "+" -< ()
> decrease <- link "-" -< ()
> number <- state (0 :: Integer) -< \n -> n + (if increase then 1 else 0) - (if decrease then 1 else 0)
> label -< show number
Тестируем:
*Tests> server test2
*** 0 ***
+ <RL()|>
- <L()|>
0
>>> 0 RL()|
*** 1 ***
+ <RL()|>
- <L()|>
1
>>> 1 RL()|
*** 2 ***
+ <RL()|>
- <L()|>
2
>>> 2 L()|
*** 3 ***
+ <RL()|>
- <L()|>
1
>>>
Третий тест проверяет, можем ли мы использовать сложные виджеты как кирпичики для ещё более сложных. Мы повторяем предыдущий тест дважды, независимо:
> test3 =
> proc () ->
> do test2 -< ()
> test2 -< ()
Проверка:
*Tests> server test3
*** 0 ***
+ <RRRL()|>
- <RRL()|>
0
+ <RL()|>
- <L()|>
0
>>> 0 RRRL()|
*** 1 ***
+ <RRRL()|>
- <RRL()|>
1
+ <RL()|>
- <L()|>
0
>>> 1 L()|
*** 2 ***
+ <RRRL()|>
- <RRL()|>
1
+ <RL()|>
- <L()|>
-1
>>> 2 RRRL()|
*** 3 ***
+ <RRRL()|>
- <RRL()|>
2
+ <RL()|>
- <L()|>
-1
>>> 3 RL()|
*** 4 ***
+ <RRRL()|>
- <RRL()|>
2
+ <RL()|>
- <L()|>
0
>>>
Четвёртый тест проверяет, как работают ветвления. Здесь у нас некий "визард" с двумя страницами, на первой - первый тест, на второй - соответственно, второй; плюс есть ссылка для переключения.
> test4 =
> proc () ->
> do switch <- link "switch" -< ()
> displayFirst <- state True -< if switch then not else id
> if displayFirst
> then do label -< "first page"
> test1 -< ()
> returnA -< ()
> else do label -< "second page"
> test2 -< ()
Я вставил в одно место
returnA -< (), потому что у test1 и test2 разные выходные типы.Прогон:
*Tests> server test4
*** 0 ***
switch <RRRRL()|>
first page
+ <RRRL()|>
0
refresh <RRL()|>
>>> 0 RRRL()|
*** 1 ***
switch <RRRRL()|>
first page
+ <RRRL()|>
1
refresh <RRL()|>
>>> 1 RRRRL()|
*** 2 ***
switch <RRRRL()|>
second page
+ <RL()|>
- <L()|>
0
>>> 2 L()|
*** 3 ***
switch <RRRRL()|>
second page
+ <RL()|>
- <L()|>
-1
>>> 3 RRRRL()|
*** 4 ***
switch <RRRRL()|>
first page
+ <RRRL()|>
1
refresh <RRL()|>
>>>
Пятый тест проверяет, можем ли мы передавать сигналы назад - тому виджету, который в тексте раньше:
> test5 =
> proc () ->
> do rec {label -< show number;
> number <- state (0 :: Integer) -< if clicked then (+ 1) else id;
> clicked <- link "+1" -< ()}
> link "refresh" -< ()
Можем:
*Tests> server test5
*** 0 ***
0
+1 <RL()|>
refresh <L()|>
>>> 0 RL()|
*** 1 ***
1
+1 <RL()|>
refresh <L()|>
>>>
Наконец, пара тестов на "хамелеончика". Первый тест - допустим, некоторая функция генерирует заданное число текстовых строк:
> labels :: Integer -> Widget () ()
> labels 0 = returnA
> labels n =
> proc () ->
> do labels (n-1) -< ()
> label -< show n
Можем ли мы сделать это число динамическим - не прошитым в виджет, а определяемым во время работы? Можем. Вот пример; здесь единственная ссылка увеличивает это самое число:
> test6 =
> proc () ->
> do clicked <- link "+" -< ()
> num <- state (0 :: Integer) -< if clicked then (+ 1) else id
> chameleon (labels 0) -< ((), Just $ labels num)
И вот как это работает:
*** 0 ***
+ <L()|>
>>> 0 L()|
*** 1 ***
+ <L()|>
1
>>> 1 L()|
*** 2 ***
+ <L()|>
1
2
>>> 2 L()|
*** 3 ***
+ <L()|>
1
2
3
>>>
Второй тест. Пусть, опять же, функция возвратит нам заданное число ссылок. На выходе нашего виджета будет номер ссылки, по которой пользователь щёлкнул:
> links :: Integer -> Widget () (Maybe Integer)
> links 0 = arr $ const Nothing
> links n =
> proc () ->
> do mI <- links (n-1) -< ()
> last <- link (show n) -< ()
> returnA -< if last then Just n else mI
Вставим её в наш виджет, так, чтобы щелчок по одной из этих "динамических" ссылок показывал нам страницу с номером и ссылкой "назад".
> test7 =
> proc () ->
> do rec {(answer, _) <- state (Nothing, Nothing) -< \(_, b) -> (b, prevAnswer);
> prevAnswer <-
> case answer of
> Nothing ->
> do clicked <- link "+" -< ()
> num <- state (0 :: Integer) -< if clicked then (+ 1) else id
> chameleon (links 0) -< ((), Just $ links num)
> Just n ->
> do label -< show n
> link "back" -< ()
> returnA -< Nothing;}
> returnA -< ()
Обратите внимание на фокус с виджетом
state - он предназначен для задержки нового состояния - если мы попытаемся установить новое состояние сразу, то страница благополучно зависнет (что вполне разумно).
*Tests> server test7
*** 0 ***
+ <RL()|>
>>> 0 RL()|
*** 1 ***
+ <RRL()|>
1 <RL()|>
>>> 1 RRL()|
*** 2 ***
+ <RRRL()|>
1 <RRL()|>
2 <RL()|>
>>> 2 RRRL()|
*** 3 ***
+ <RRRRL()|>
1 <RRRL()|>
2 <RRL()|>
3 <RL()|>
>>> 3 RRL()|
*** 4 ***
2
back <L()|>
>>> 4 L()|
*** 5 ***
+ <RRRRL()|>
1 <RRRL()|>
2 <RRL()|>
3 <RL()|>
>>>
Здесь мы увеличили количество ссылок до трёх, а затем щёлкнули по второй.
Вопросы? Замечания?
Смотрю новый сериал FlashForward. Сюжет пересказывать не буду, желающие узнают всё необходимое, например, вот здесь. Просто соберу в кучу некоторые вопросы и теории, а потом посмотрим - что из этого оправдается, и на что будет дан ответ.
- То, в чём я не сомневаюсь.
- Никаких инопланетян, ангелов, чертей, вампиров (увы), господа бога или Святого Ника. Если другой разум - отличный от человеческого - появится в последней серии, то это будет совершенно дикий рояль в кустах. Если не в последней - тот факт, что именно эти гады виноваты в происшедшем, станет, к сожалению, очевидным.
- Никакой мистики вообще. Если бы в этом сериале была возможна мистика - она бы уже проявилась, три серии, как-никак.
- Не природный катаклизм. Слишком легко списать всё на стихию. Обесценивает всё проведённое расследование.
- Теории
- Теория темпорального эха. Пока что все персонажи считают, что, кто бы ни стоял за происшедшим, он уже всё сделал. Нажал кнопку, или что там ещё. Но, поскольку мы и так имеем дело с временной аномалией, почему не допустить, что всё произойдёт 29 марта (30-го в Европе), а имевшее место затемнение - просто эхо, откатившееся НАЗАД во времени.
- Теория шахматиста. Только у меня возникает ощущение, что наши ФБР-овцы не столько расследование ведут, сколько реагируют на ниточки, которые кто-то осознанно дёргает? И если да, то не может ли быть так, что цель затемнения - или одна из целей - это добиться того, чтобы главные герои сделали что-то или оказались где-то?
- Вопросы
- Может ли вообще наступить то будущее, которое было в видениях? Ни в жисть не поверю, что не найдётся человек, достаточно упрямый, чтобы просто назло судьбе сделать наоборот. В конце концов, для этого не много нужно. Один из ФБР-овцев говорил, если не вру, что в его видении он смотрел новости по телевизору. Что ему будет стоить выключить телевизор в соответствующее время, тем более, что оно хорошо известно. С другой стороны, некоторые, по-видимому, должны к этому времени умереть. Возможно, события подгадают таким образом, чтобы именно эти упрямцы и умерли?
- Дима Но. С одной стороны, похоже, он умрёт. С другой стороны, слишком уж активно нас подталкивают к тому, что именно это и должно случиться. И именно это, в свою очередь, напоминает о теории шахматиста - сначала Димке подсунули шерифшу, которая тут же и умерла, потом последовал звонок фиг знает откуда. Ему как бы не говорят прямым текстом (звонок по телефону не есть прямой текст, ибо его таинственность внушает сомнения), а подводят к такому убеждению. Возможно, шахматист хочет, чтобы Димка верил, что умрёт - как, скажем, в Drive профессор верил в свою обречённость, и в результате действовал так, как никогда бы не подумал действовать, будь его здоровье в порядке.
- Нулевой. Поведение этого товарища очень странно. Если он не знал о затемнении - то почему вёл себя так спокойно? Не паниковал, даже не торопился найти какое-нибудь укрытие (то, что сделал бы я). Спокойно прошёл по трибуне и удалился. Может быть, он душевнобольной? Интересно, аутисты посещают спортивные мероприятия, или им до фени? Если он знал - почему он вообще оказался на этом стадионе? Почему не отсиделся дома? Если он хотел понаблюдать за толпой - кто мешал ему выбрать местечко на крыше небоскрёба и смотреть на какую-нибудь оживлённую улицу? Может, он хотел запечатлеться на камеру? Согласуется с теорией шахматиста, но что-то в этом направлении никакого продвижения пока не видно. Может быть, он не ЗНАЛ о затемнении, но быстро всё понял? Скажем, если это талантливый физик, который зарание просчитал, что что-то подобное может быть результатом, например, включения Большого Гудронного Уклайдера? Кстати, это согласуется с теорией темпорального эха. А по телефону он тогда разговаривал со своим приятелем, который тоже был в курсе этой теории - Алё, Вовка? У тебя там такой же бардак, как и здесь? А что я тебе говорил? Вот-вот, и не зря мы те таблетки принимали, а то тоже валялись бы по земле.
- Ди Гиббонс. Разговор по телефону с Нулевым согласуется с гипотезой, что они хотели быть обнаруженными (трудно было бы представить, что ФБР не узнает рано или поздно, что во время затемнения случился разговор по телефону) - и с теорией шахматиста. С другой стороны, как сказано в предыдущем пункте, может согласовываться и с теорией темпорального эха. Что о нём знает Чарли и почему она называет Гиббонса "Ди"? Не "Дэвид Гиббонс", не "мистер Гиббонс", а именно "Ди"? Может быть, имя Гиббонса было где-нибудь написано? Умеет ли Чарли в её возрасте читать? Вероятно, да. Не является ли "Ди Гиббонс" кивком в сторону Дейва Гиббонса, соавтора "Вотчменов"? Последнее уже к загадкам сериала не относится.
- Герр Гейер. Уж больно аккуратно он их развёл. Кто знает, может быть, его освобождение было одной из целей шахматиста (буде таковой существует)?
- Сам феномен бодрствования во время затемнения. Поведение Нулевого может быть объяснено разными причинами, а вот его телефонный разговор с другим бодрствующим практически доказывает, что они сохранили сознание не случайно. Они что-то делали для этого. С другой стороны, мальчик в сомалийском флэшбеке точно был никак не связан с организаторами безобразия. У него шрам на лице - может быть, это как-то связано с его бодрствованием? Согласуется с гипотезой о психическом заболевании. Возможно, Нулевой и Ди Гиббонс страдают одним и тем же заболеванием? Может быть, они подружились на сеансе групповой терапии и когда началось затемнении, один просто рефлекторно позвонил своему единственному другу? Согласуется с тем, что поведение Гиббонса во время попытки ареста было, скажем так, неадекватным. Однако неужели подобное - явно редкое - заболевание оставит после себя лишь небольшой шрам на лбу, причём в стране, где доктор Хаус не живёт?
- Что это за фаллический символ стоял в Сомали и что за белая субстанция из него выплеснулась? Да, я осознаю, что вопрос пошлый.
- Те спецназовцы в видении Марка. Они шли за ним? Или за Стэном? Если бы Стэн находился у себя, то он был бы поблизости - Марк его, помнится, даже спрашивал, не видел ли он чего-нибудь. Он в это время сидел в сортире, но киллеры могли этого не знать.
Текущие настройки mencoder-а для конвертации видео на айфон:
mencoder источник.avi -o результат.mp4 -vf dsize=480:320:0,scale=-8:-8,harddup -oac faac -faacopts mpeg=4:object=2:raw:br=128 -of lavf -lavfopts format=mp4 -ovc x264 -x264encopts nocabac:level_idc=30:bframes=0:global_header:threads=auto:subq=5:frameref=6:partitions=all:trellis=1:chroma_me:me=umh:bitrate=500:no8x8dct
Вот так вот.
Итак, оно кончилось. 759 место, дальше не прохожу. Решил первую задачу и первую часть третьей - оно таки обломалось на large set. Вывод: с некоторыми базовыми алгоритмами у меня, всё-таки, плохо.
когда набрал в терминале "which watch".
compose-trans-0.0
Сделан по мотивам вот этого поста. Очень сильно отрефакторено и упрощено.
делал функцию "Родительский контроль" в винде? Неужели он не мог посмотреть хотя бы на макось? Как вообще в его микроскопический мозг пришла мысль, что родитель должен задавать не общее время, которое чадо проводит за компом, а конкретные часы? Не "два часа в день", а "с 17:30 до 19:30"? Или эта хуйня разрабатывалась изначально для использования в пенитенциарных учреждениях?
Почему, интересно, в макоси, да и в любом другом юниксе, никого не ебёт, какие программы юзер установит для себя лично, главное, чтобы не пытался лезть в чужие данные - а в этой куче дерьма под названием Vista по умолчанию установка софта запрещена всем не-админам? Это надо понимать как признание, что ихний выкидыш представляет собой глюк на глюке, который упадёт от первого залетевшего дятла? Какого хрена? Раньше я думал, что под админом работают только идиоты. Похоже, что в винде другого варианта вообще нет.
Ощущение такое, что виста - это не ОС, а демка. А винда Home Basic, которая шла вместе с компом - демка от демки. Повбывав бы. Уроды. Все.
Началось, как обычно, с мелочи. Дизайнеры сделали новую модель игрока, заменив устрашающий солдафонский костюм на футболку и джинсы, не менее устрашающие. В какой-то момент игрок посмотрел в небо, слегка отклонившись при этом назад. С другой позиции сразу стало видно, как автомат, висевший у игрока за спиной, прошёл у него между ногами и нагло торчит дулом аккурат из ширинки. Особо впечатлительные крестились и украдкой прикасались к томику Фрейда.
А потом пришла дверь. Обыкновенная дверь, которая просто не открывалась. До тех пор, пока её не переключали из режима физики в режим анимации, в котором она медленно открывалась, быстро захлопывалась и делала goto :begin. И надо же было именно на ней тестировать новый режим - когда работает и физика, и анимация сразу. Дверь начала исполнять танец пьяного ёжика вокруг косяка, вылетая далеко за запланированные пределы её перемещений. Испуганный девелопер выключил анимацию. Дверь пришла в себя, снялась с петель и, издевательски вращаясь вокруг вертикальной оси, улетела за горизонт.
Под конец сисадмин попросил зашедшего к нему по какой-то надобности директора налить ему чаю, потому как он сам, видите ли, до сих пор не сумел разобраться в управлении чайником.
Что-то будет завтра...
Если кому интересно, то начистить мне чайник можно здесь: http://migmit.mybrute.com