В DisplayFormatter нельзя добавлять поля, которые используются в бизнес-логике
Кто запретит? Компилятор?
только инфраструктурную информацию, типа подключения к базе данных, логер и настройки из yml (кстати, как вы будете хранить эту информацию в ваших чистых функциях?)
Какое ещё подключение к базе данных в классе (или функции, неважно), отвечающем за форматирование имени данного ему юзера? Это чтобы макаронный код потом веселее разгребать было?
Самое главное, как вы будете определять, какую функцию передавать в другую функцию в случае, если например, для одних настроек приложения надо использовать DisplayFormatter, а для других - RTFFormatter?
Так и буду.
data Options = Options
{ useRtf :: Bool
} deriving (Eq, Show, Generic, ParseCliOptoins)
main :: IO ()
main = do
Options{..} ← getCliOptions
doThings (if useRtf then rtfFormatter else displayFormatter)
Есть такой же полиморфизм, что-то типа наследования (классы и инстансы)
На всякий случай, ещё раз подчеркну, что классы — это объявления контрактов/концептов, а инстансы — сообщение компилятору, что данный тип (или множество типов) удовлетворяет контракту, и описание, как именно они это делают. С ООП тут есть некоторое пересечение, но очень отдалённое.
Например, может быть класс и инстансы
class Mult left right result | left, right → result where
multiply :: left → right → result
instance Mult (Matrix m n) (Matrix n k) (Matrix m k) where
multiply = ...
instance Mult (Vect n) (Vect n) (Vect n) where
multiply = ...
где в первой строке Mult left right result означает, что «контракт» описывает отношения трёх типов, а | left, right → result означает, что первые два типа автоматически определяют третий единственным образом (и компилятор это проверяет для инстансов и пользуется при тайпчекинге/выводе типов).
Я затрудняюсь выразить ровно это на ООП с такой же степенью симметрии.
Или можно (но не нужно) на тайпклассах делать арифметику Пеано, и чтобы компилятор потом проверял, что соответствующие равенства выполняются.
В качестве бонуса более сильный статический анализ компилятором за счет типов.
Да. Ну и опять же, это тут в коде выше везде IO, потому что задача по факту и состоит из одного IO. На практике даётся больше гарантий, что код (не) делает, за счёт этих самых типов, да.
Минусы, я думаю, тоже есть.
Зависит от задач, конечно, но я ни разу не ловил себя на желании «эх вот бы щас тут ООП-классик бы навернуть».
Надо будет функции передавать параметр конкретной операции? Или есть более простой механизм?
Зависит от более общего контекста. Можно так, да:
data Input
= EncodeFrame VideoFrame
| NotifyMissingInput
| ReconfigureBitrate Bitrate
type Encoder = Input → IO (Either Error BitStream)
Можно
data Encoder m = Encoder
{ encodeFrame :: VideoFrame → m (Either Error BitStream)
, notifyMissing :: m ()
, reconfigureBitrate :: Bitrate → m ()
}
— то есть, тупо рекорд (структура) с разными функциями, которая может создаваться через
vp8encoder :: Params → IO (Encoder IO)
vp8encoder params = do
commonRef ← newIORef ...
let encodeFrame = ...
notifyMissing = ...
reconfigureBitrate = ...
pure $ Encoder {..}
и эта commonRef может капчуриться в определения любых функций внутри, и шариться между ними.
Это выглядит совсем как ООП, только, опять же, так как это полноценные first-class-citizen-данные, можно легко их собирать из отдельных кусочков и легко модифицировать. Паттерны и языковые фичи вроде миксинов просто становятся не нужны.
криптография 2004-2006 года - это старый и сложный код?
Старый, но несложный.
все прекрасно собралось
Добавьте оптимизации:
% g++ -v |& grep "gcc version"
gcc version 14.2.1 20241221 (Gentoo 14.2.1_p20241221 p7)
% g++ -Wall -Wextra -O3 -DDERS_CPU=cpu -c key.cpp
[...]
In file included from uint.hpp:22,
from key.hpp:19,
from key.cpp:11:
In destructor 'sh_ptr<T>::~sh_ptr() [with T = UInt]',
inlined from 'void Key::computeRemainders(ShUInt, int, std::vector<sh_ptr<UInt> >&)' at key.cpp:57:1:
sh_ptr.hpp:93:30: warning: '*(sh_ptr<UInt>::Rep*)<unknown>.sh_ptr<UInt>::Rep::refs' may be used uninitialized [-Wmaybe-uninitialized]
93 | ~sh_ptr() { if (--rep->refs==0) delete rep; }
| ~~~~~^~~~
и отлично работает!
Не могу проверить, потому что хз с чем это запускать вообще.
Но зато ошибки видны просто невооружённым глазом. Например,
else return ShUInt(); // NULL
вызывает сначала конструктор sh_ptr<UInt> с нулевым указателем ptr, а потом его удаляет через в итоге fixed_alloc::free(ptr), который…
Ой-вей, поздравляю с записью по нулевому указателю.
ЗЫ
ЗЫ — чувак написал 1400 строк кода, из которых треть — переизобретение своего наколенного boost::shared_ptr и прочего подобного, в которых сходу находятся ошибки, и пытается этим опровергнуть наличие проблем с миграцией на новые компиляторы у тех, кого строк кода не 1400, а 140000000.
Это, конечно, смешно.
может дело немножно в руках?
Это про любую технологию сказать можно. Дело всегда в руках, технологии всегда шикарные. Не можешь писать производительный код на JS? Да у тебя просто руки кривые.
Детская травма от необходимости явно писать типы нетривиальных match-конструкций в определении функций, когда ну вот же оно тут написано прямо в типе функции ну что ты петух тупой что ли блин.
За счёт того, что в агде паттерн-матчинг сделан ну вот прям в определении функций, там это всё куда лучше выглядит.
Что, конечно, не отменяет куда более лучшей автоматизации в коке, поэтому имеем то, что имеем.
Спасибо, но я все еще не понимаю, а где хранится FallbackState для каждого энкодера в системе?
Он хранится для каждого созданного «экземпляра» fallback-энкодера в замыкании соответствующей функции. Каждый раз, когда я пишу
encoder ← swFallbackEncoder vp9hw vp9sw
то у меня «запускается» тело swFallbackEncoder, которое создаёт (newIORef) мутабельную переменную (isFallback), где лежит FallbackState (CanUseHW по умолчанию), и возвращает лямбду, которая на каждый фрейм проверяет эту захваченную переменную.
На плюсах это выглядело бы примерно как
auto swFallbackEncoder(auto hw, auto sw)
{
auto shouldFallback = std::make_shared<bool>(false);
return [=](VideoFrame frame)
{
if (*shouldFallback)
return sw(frame);
auto res = hw(frame);
if (!res)
{
res = sw(frame)
*shouldFallback = true;
}
return res;
};
}
Вот эта mkEncoder она могла вернуть fallback врапер, а могла просто энкодер. Как вы потом ее результат вызваете?
Как функцию :]
runWithConfig config = do
maybeEnc ← mkEncoder (encoderName config)
case maybeEnc of
Nothing → putStrLn "uh oh"
Just enc → enc frame -- вызываю!
Можно написать тестовый код вроде
vp9swEncoder :: IO (Maybe Encoder)
vp9swEncoder = pure $ pure $ \frame → putStrLn "doing sw..." >> pure (Right $ show frame)
vp9hwEncoder :: IO (Maybe Encoder)
vp9hwEncoder = pure $ pure $ \frame → putStrLn "trying hw…" >> pure (Left "unable to encode")
и потом
main :: IO ()
main = do
-- лень проверять just/nothing, я знаю, что там just
Just enc1 ← mkEncoder "vp9"
Just enc2 ← mkEncoder "vp9"
enc1 10 >>= print
enc1 11 >>= print
enc2 20 >>= print
enc2 21 >>= print
выведет
trying hw…
doing sw...
Right "10"
doing sw...
Right "11"
trying hw…
doing sw...
Right "20"
doing sw...
Right "21"
обратите внимание — trying hw для каждого отдельного энкодера печатается только раз.
Далее создаем отдельный класс, который будет заниматься только одной задачей - формированием информации для вывода на дисплей.
Зачем для этого класс? Какой у него стейт? Могу ли я один инстанс класса DisplayFormatter дёргать из разных тредов? Могу ли я разные инстансы дёргать из разных тредов? Эквивалентен ли DisplayFormatter, отформатировавший сто юзеров, свежесозданному?
А можно просто сделать чистую функцию formatUser : User → String, и там этих вопросов нет. И передавать эти функции как аргументы другим функциям (если очень хочется звучать умно, можно это тоже называть DI).
Блин, извините, я идиот, зачем-то флаги держу, когда можно без флагов, и просто в стейте хранить саму функцию, которую надо вызывать. Так гораздо чище, и в кои-то веки пригодился Control.Monad.mfix:
swFallbackEncoder :: Encoder → Encoder → IO Encoder
swFallbackEncoder hwEncoder swEncoder = do
runner ← mfix $ \runner → newIORef $ \frame → hwEncoder frame >>= \case
Right bs → pure $ Right bs
Left err → writeIORef runner swEncoder >> swEncoder frame
pure $ \frame → readIORef runner >>= ($ frame)
В коде ключевые слова class, instance... Ну допустим это другое.
Это действительно другое.
class — это обобщение ООПных интерфейсов или плюсовых концептов. Концепт «итератор» был бы классом. «Моноид» на самом деле является классом. «Любая монада, поддерживающая возможность хранить стейт типа Foo», является классом.
Инстанс — это объявление, что данный тип соответствует данному интерфейсу/концепту, и реализация требуемых интерфейсом/концептов методов/типов.
Часть — код редактора (который определяет тайпкласс Tool,и непоказанная часть по использованию, потому что грузить экзистенциалами читателя не стоит). Часть — код клиентов (определяющих конкретные тулы, это каждая пара data/instance).
Соседние ораторы показали много разных сложных примеров, а я покажу рабоче-колхозно:
type EncodeResult = Either Error BitStream
type Encoder = VideoFrame → IO EncodeResult
-- создаёт энкодер, загружая libvpx и потенциально фейлясь
vp9swEncoder :: IO (Maybe Encoder)
vp9swEncoder = undefined
-- ну дрова уж точно могут зафейлиться при инициализации
vp9hwEncoder :: IO (Maybe Encoder)
vp9hwEncoder = undefined
data FallbackState = CanUseHW | ShouldFallback
swFallbackEncoder :: Encoder → Encoder → IO Encoder
swFallbackEncoder hwEncoder swEncoder = do
isFallback ← newIORef CanUseHW
pure $ \frame → readIORef isFallback >>= \case
ShouldFallback → swEncoder frame
CanUseHW → hwEncoder frame >>= \case
Right bs → pure $ Right bs
Left _ → writeIORef isFallback ShouldFallback >> swEncoder frame
Дальше это можно обмазать типами, чтобы swFallbackEncoder можно было создать только из пары SW и HW-энкодеров, но это предлагается читателю в качестве упражнения.
И в ООП вы или создаете VP8SWEncoder или H265HWEncoder или SoftwareFallbackEncoder(VP9SWEncoder, VP9HWEncoder). А потом просто вызвыаете Encode() у инстанса не думая о том, какой зоопарк кодеков у вас на самом деле.
А в чём вопрос? Просто возвращаете функцию.
mkEncoder :: String → IO (Maybe Encoder)
mkEncoder "vp9" = do
vp9swResult ← vp9swEncoder
vp9hwResult ← vp9hwEncoder
case (vp9hwResult, vp9swResult) of
(Just vp9hw, Just vp9sw) → pure <$> swFallbackEncoder vp9hw vp9sw
(_, _) → pure $ msum [vp9hwResult, vp9swResult]
mkEncoder "vp9:force-sw" = vp9swEncoder
mkEncoder "vp8" = vp8swEncoder
mkEncoder "h265" = h265hwEncoder
mkEncoder _ = pure Nothing
Если бы меня кто-то спрашивал, я бы сказал, что преимущества ООП раскрываются в предметных областях, которые хорошо матчатся на объектную модель (жира, например, как программный продукт: таска, проект, исполнитель, то-сё).
Звучит снова как ФП.
Я читал Domain Modeling Made Functional, и читал более классическую книгу Эванса. Если в первой всё было просто и понятно, хоть сейчас устраивайся в кровавый тырпрайз, то после второй я, по-моему, стал понимать даже меньше.
class Tool t m where
processPick :: t → Ctx → Point → m ()
handleSelection :: t → [Object] → m ()
data BackdoorCCP = BackdoorCCP
{ address :: String
, port :: Int
}
instance Tool BackdoorSendToCCPServers IO where
processPick bd _ p = sendToCCP bd $ "User clicked at " <> p
handleSelection = ...
data FilterClicks base = FilterClicks
{ filterPred :: Ctx → Point → Bool
, baseTool :: base
}
instance Tool t m => Tool (FilterClicks t) m where
processPick FilterClicks{..} ctx p
| filterPred ctx p = processPick baseTool ctx p
| otherwise = pure ()
handleSelection = ...
data TestSpy = TestSpy
instance Tool TestSpy (WriterT [(Point)] m) where
processPick _ _ p = tell [p]
handleSelection = ...
Функциональное программирование более гибкое, соглашусь. Податливо, как пластилин. Очень удобно для научных задачек и сольного программирования. НО ЗАВОДЫ ИЗ ПЛАСТИЛИНА НА СТРОЯТ.
Ага. Именно поэтому верифицированное ядро SeL4 в первую очередь сделано на хаскеле (с полуэкстракцией в C). Или поэтому смарт-контракты с требованиями доверия им делаются на хаскеле и верифицируются на коке/изабелле/етц.
…которые в него вступили по причинам традиций, социального давления и так далее, не понимая, что лично им в нём будет хуже, чем описывается в дискурсе.
На самом деле тут надо бы ещё как-то демонстрировать своё отношение к этим книгам, потому что прочитанный и стоящий на полочке Поппер или Фукуяма ничего не говорят о том, насколько вы с ними согласны.
Кто запретит? Компилятор?
Какое ещё подключение к базе данных в классе (или функции, неважно), отвечающем за форматирование имени данного ему юзера? Это чтобы макаронный код потом веселее разгребать было?
Так и буду.
На всякий случай, ещё раз подчеркну, что классы — это объявления контрактов/концептов, а инстансы — сообщение компилятору, что данный тип (или множество типов) удовлетворяет контракту, и описание, как именно они это делают. С ООП тут есть некоторое пересечение, но очень отдалённое.
Например, может быть класс и инстансы
где в первой строке
Mult left right resultозначает, что «контракт» описывает отношения трёх типов, а| left, right → resultозначает, что первые два типа автоматически определяют третий единственным образом (и компилятор это проверяет для инстансов и пользуется при тайпчекинге/выводе типов).Я затрудняюсь выразить ровно это на ООП с такой же степенью симметрии.
Или можно (но не нужно) на тайпклассах делать арифметику Пеано, и чтобы компилятор потом проверял, что соответствующие равенства выполняются.
Да. Ну и опять же, это тут в коде выше везде
IO, потому что задача по факту и состоит из одногоIO. На практике даётся больше гарантий, что код (не) делает, за счёт этих самых типов, да.Зависит от задач, конечно, но я ни разу не ловил себя на желании «эх вот бы щас тут ООП-классик бы навернуть».
Зависит от более общего контекста. Можно так, да:
Можно
— то есть, тупо рекорд (структура) с разными функциями, которая может создаваться через
и эта
commonRefможет капчуриться в определения любых функций внутри, и шариться между ними.Это выглядит совсем как ООП, только, опять же, так как это полноценные first-class-citizen-данные, можно легко их собирать из отдельных кусочков и легко модифицировать. Паттерны и языковые фичи вроде миксинов просто становятся не нужны.
Старый, но несложный.
Добавьте оптимизации:
Не могу проверить, потому что хз с чем это запускать вообще.
Но зато ошибки видны просто невооружённым глазом. Например,
вызывает сначала конструктор
sh_ptr<UInt>с нулевым указателемptr, а потом его удаляет через в итогеfixed_alloc::free(ptr), который…, который…
Ой-вей, поздравляю с записью по нулевому указателю.
ЗЫ — чувак написал 1400 строк кода, из которых треть — переизобретение своего наколенного
boost::shared_ptrи прочего подобного, в которых сходу находятся ошибки, и пытается этим опровергнуть наличие проблем с миграцией на новые компиляторы у тех, кого строк кода не 1400, а 140000000.Это, конечно, смешно.
Это про любую технологию сказать можно. Дело всегда в руках, технологии всегда шикарные. Не можешь писать производительный код на JS? Да у тебя просто руки кривые.
Детская травма от необходимости явно писать типы нетривиальных
match-конструкций в определении функций, когда ну вот же оно тут написано прямо в типе функции ну что ты петух тупой что ли блин.За счёт того, что в агде паттерн-матчинг сделан ну вот прям в определении функций, там это всё куда лучше выглядит.
Что, конечно, не отменяет куда более лучшей автоматизации в коке, поэтому имеем то, что имеем.
Он хранится для каждого созданного «экземпляра» fallback-энкодера в замыкании соответствующей функции. Каждый раз, когда я пишу
то у меня «запускается» тело
swFallbackEncoder, которое создаёт (newIORef) мутабельную переменную (isFallback), где лежитFallbackState(CanUseHWпо умолчанию), и возвращает лямбду, которая на каждый фрейм проверяет эту захваченную переменную.На плюсах это выглядело бы примерно как
Как функцию :]
Можно написать тестовый код вроде
и потом
выведет
обратите внимание — trying hw для каждого отдельного энкодера печатается только раз.
Зачем для этого класс? Какой у него стейт? Могу ли я один инстанс класса
DisplayFormatterдёргать из разных тредов? Могу ли я разные инстансы дёргать из разных тредов? Эквивалентен лиDisplayFormatter, отформатировавший сто юзеров, свежесозданному?А можно просто сделать чистую функцию
formatUser : User → String, и там этих вопросов нет. И передавать эти функции как аргументы другим функциям (если очень хочется звучать умно, можно это тоже называть DI).Я другой оратор и у меня другой тривиальный пример, я его ниже написал.
Блин, извините, я идиот, зачем-то флаги держу, когда можно без флагов, и просто в стейте хранить саму функцию, которую надо вызывать. Так гораздо чище, и в кои-то веки пригодился
Control.Monad.mfix:Это действительно другое.
class— это обобщение ООПных интерфейсов или плюсовых концептов. Концепт «итератор» был бы классом. «Моноид» на самом деле является классом. «Любая монада, поддерживающая возможность хранить стейт типаFoo», является классом.Инстанс — это объявление, что данный тип соответствует данному интерфейсу/концепту, и реализация требуемых интерфейсом/концептов методов/типов.
Часть — код редактора (который определяет тайпкласс
Tool,и непоказанная часть по использованию, потому что грузить экзистенциалами читателя не стоит). Часть — код клиентов (определяющих конкретные тулы, это каждая параdata/instance).Соседние ораторы показали много разных сложных примеров, а я покажу рабоче-колхозно:
Дальше это можно обмазать типами, чтобы
swFallbackEncoderможно было создать только из пары SW и HW-энкодеров, но это предлагается читателю в качестве упражнения.А в чём вопрос? Просто возвращаете функцию.
Звучит снова как ФП.
Я читал Domain Modeling Made Functional, и читал более классическую книгу Эванса. Если в первой всё было просто и понятно, хоть сейчас устраивайся в кровавый тырпрайз, то после второй я, по-моему, стал понимать даже меньше.
Как один из кучи вариантов.
isabelle/hol
Мне не зашло, но люди пользуются (а меня и от кока чё-т тошнит).
Да один фиг, даже обычное сожительство тоже переоценено.
На удалёнке от 10 до 30 тысяч долларов на руки, нафиг эти ваши офисы?
Ага. Именно поэтому верифицированное ядро SeL4 в первую очередь сделано на хаскеле (с полуэкстракцией в C). Или поэтому смарт-контракты с требованиями доверия им делаются на хаскеле и верифицируются на коке/изабелле/етц.
Почему не подходит? Вполне применяю его широко, от написания на нём компиляторов до перекладывающих жсоны опердней.
…которые в него вступили по причинам традиций, социального давления и так далее, не понимая, что лично им в нём будет хуже, чем описывается в дискурсе.
Или, более кратко, «брак переоценен».
На самом деле тут надо бы ещё как-то демонстрировать своё отношение к этим книгам, потому что прочитанный и стоящий на полочке Поппер или Фукуяма ничего не говорят о том, насколько вы с ними согласны.