Обновить
1
1.6

Специалист по теории типов USB-кабелей

Отправить сообщение

В 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), который…

      static void free(void* ptr)
      {
       fixed_alloc_private::void_alloc<SIZE>::free(ptr);
      }

, который…

       static void free(void* ptr)
       {
        *(void**)ptr=head;
        head=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 = ...

Как один из кучи вариантов.

isabelle/hol

Мне не зашло, но люди пользуются (а меня и от кока чё-т тошнит).

Да один фиг, даже обычное сожительство тоже переоценено.

На удалёнке от 10 до 30 тысяч долларов на руки, нафиг эти ваши офисы?

Функциональное программирование более гибкое, соглашусь. Податливо, как пластилин. Очень удобно для научных задачек и сольного программирования. НО ЗАВОДЫ ИЗ ПЛАСТИЛИНА НА СТРОЯТ.

Ага. Именно поэтому верифицированное ядро SeL4 в первую очередь сделано на хаскеле (с полуэкстракцией в C). Или поэтому смарт-контракты с требованиями доверия им делаются на хаскеле и верифицируются на коке/изабелле/етц.

Хаскель не предлагать как язык широкого применения не проходит

Почему не подходит? Вполне применяю его широко, от написания на нём компиляторов до перекладывающих жсоны опердней.

…которые в него вступили по причинам традиций, социального давления и так далее, не понимая, что лично им в нём будет хуже, чем описывается в дискурсе.

Или, более кратко, «брак переоценен».

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

Информация

В рейтинге
1 551-й
Зарегистрирован
Активность