Обновить
1
1.6

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

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

Ну и пусть. Если программист пишет новый класс extends Order, значит ему так надо для его задачи.

Если программист делает ошибки, значит ему так надо для его задачи.

У меня это задано с помощью функции isFinalizeAllowed, не вижу причин почему это надо делать именно разными типами.

Чтобы компилятор видел, опять же.

Описать 10 типов сложнее, чем 1.

Какие 10 типов у меня указано выше? У меня выше ровно два типа: тип, описывающий возможные состояния, и тип заказа. На сдачу ещё можно функцию, вычисляющую тип billing address'а по состоянию, посчитать, хз, но это тоже O(1) от количества состояний.

Отмена может быть возможна из 6 разных состояний, с типами придется их все указывать в аргументе и обновлять при добавлении нового разрешенного для отмены.

С чего бы?

CanCancel :: State → Bool
CanCancel Finalized = True
CanCancel Building = True
CanCancel _ = False

cancel :: CanCancel st ~ True ⇒ Order st → Order Cancelled

Добавляете просто ещё один кейс в CanCancel.

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

Нет, зачем?

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

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

Это практически то, о чём пишут всякие дяди бобы, только сделанное по-нормальному и проверяемое компилятором.

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

Абстракция — не самоцель, не все абстракции одинаково полезны. Я не хочу абстрагировать непосредственную бизнес-задачу, чтобы она была где-то внутри, а снаружи торчали абстрактные фабрики декораторов спринг-бинов.

Они подбираются под бизнес-требования, но выражают формат данных, которые лежат в переменной.

Формат данных и его семантику. Целое число. Положительное число. Делитель вон того числа.

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

И какие инструменты дадут мне по рукам за написание ерунды и помогут писать правильный код прямо во время написания кода, до того, как я не то что код в продакшен выкатил, а даже файл сохранил?

Ну так и с ФП можно написать только 1 тип, а не как у вас.

Ну да. И с ООП можно всё написать в одном классе, прямо в public static void Main.

Только в ООП тяжело требовать от компилятора проверок, а в типизированном ФП — легко.

Вот я и говорю, если мы пишем новый код, например добавляем метод shipOrder, то никакой существующий код нам не помешает случайно написать shipOrder :: Order Paid → Order Finalized.

Помешает, если алгебра работы с ордерами прописана в отдельном модуле (который менять не надо, и который не экспортирует кишки ордеров), а конкретные сценарии работы с ними вы описываете снаружи.

наследуем его, переопределяем, если надо для него enter() и exit() - то что будет обрабатываться при переходе в стейт и покидании его.

Подождите, я не понимаю. Как вы разделяете, что идёт в enter «следующего» состояния, а что — в exit «предыдущего»?

Вот у меня есть, опять же, пусть простой случай — заказ в состоянии Finalized. При переходе в состояние Paid надо проверить, что платёжные реквизиты норм, и выслать письмо покупателю. Что тут куда идёт?

дополняем fsm методом gotoSaving()

Кто такой fsm? Это какой-то класс-менеджер? Инстанс конкретной стейтмашины, построенной по данным бизнес-правилам?

реализуем логику перехода в новый стейт, там, где это надо по задаче

«А теперь рисуем сову»

Кто это вызывает? Это вызывается снаружи fsm? Какова его роль тогда? В чём разница между gotoSaving и Saving::enter?

Тяжело представить. Можете всё-таки показать пример? Я вот потрудился и набросал немного наиболее характерного кода.

Никак, так же как у вас. Сколько сделали типов, столько и будет. Зачем их ограничивать?

У меня количество состояний ограничено, новые добавить нельзя «снаружи», без модификации типа State. У вас — можно.

new с копированием полей. Потому так никто и не делает.

Тяжеловесный синтаксис мешает выражать мысли, не удивлён.

Так мы сами задаем осмысленные операции, а не компилятор.

Мы задаём (постулируем) атомарные операции. Осмысленность их композиции проверяет компилятор.

Да и даже осмысленность их задания компилятор проверить может — выявляемый косяк с размером фрейма композиции энкодеров я описывал рядом.

Обычно надо сделать новую функциональность, а не вызвать существующую, потому что существующая и так уже вызывается в существующем коде.

Новая функциональность может быть сделана в терминах «описать пайплайн для заказов со скидками и промо-акциями». И тут type-driven development вместе с type-driven-рефакторингом очень помогают.

В том и дело, что finalizeOrder совсем необязательно должна ставить какой-то статус. Что надо делать, описано в бизнес требованиях.

В рамах данной модельной задачи финализированный ордер — это отдельная сущность, потому что к нему применим строго определённый (и отличный от других состояний ордеров) набор операций.

Всё в типе не опишешь.

Опишешь :]

Но даже если бы это было не так,

Там много всего, и получения нужной информации все равно надо смотреть реализацию.

— стремление снизить необходимость лезть в детали реализации ИМХО осмысленно и вполне естественно.

Точно так же, как знание того, что функция принимает int, не отменяет необходимости понимать, что она потом с ним делает, но при этом всё равно оказывается очень полезным.

Типы переменных это техническая информация, а статусы заказа это бизнес-требования.

Типы переменных тоже выражают бизнес-требования. Телефон — скорее строка (а не число) потому, что «phone 0123» ≠ «phone 123». Количество компьютеров на складе — целое число потому, что у вас не может быть пол-компьютера. Деньги в трейдинговой системе выражаются как fixed point с N разрядами после точки потому, что таковы бизнес-требования.

Можно пытаться изложить бизнес-требования средствами для технической информации, но мы возвращаемся к вопросу "зачем?".

Для ответа придётся вернуться к тезису «чтобы компилятор проверял их выполнение.»

Так в императивном коде это тоже будет отдельная новая функция, сразу бросающаяся в глаза.

Не обязательно. Это вполне может быть скрыто где-то в деталях реализации в ООП-коде. ООП-код вида

oldState = this.state;
this.state = Finalized;
this.doSomethingExpectingFinalizedState();
this.state = oldState;

просто потому, что так проще, я видел.

На хаскеле можно (и очень приятно) делать DSL'и в том числе для глубокого эмбеддеда: например, ivory.

Вы путаете конкретную задачу конкретного магазина и модельную задачу выражения стейтмашины. Вас просят решить задачу «Вася купил пять яблок за десять рублей, сколько стоит яблоко?», а вы начинаете рассказывать, что яблоки бывают разные, и вообще владелец магазина братан Васи и дал ему одно яблоко бесплатно.

if (!this.isFinalizeAllowed(order))

Рантайм-проверка же. Это совсем не то же самое.

Можно сделать классы OrderBuilding extends Order, OrderFinalized extends Order, но обычно так никто не делает.

Как тогда ограничить количество наследников-«состояний»?

И, кстати, из любопытства, как будет выглядеть создание OrderFinalized из OrderBuilding в вашем любимом языке? У них одинаковые поля, и в хаскеле я с RecordWildcards могу сделать finalize Order{..} = Order{..}, компилятор мне сам все поля перепакует (и при компиляции вообще превратит эту функцию в noop, потому что увидит, что не меняется ничего, кроме типов).

Да, значение поля в типах не проверяется. А надо? А зачем?

Чтобы компилятор проверил отсутствие заведомо бессмысленных операций или переходов.

И для того, чтобы у меня были явные подсказки, что я сейчас могу сделать дальше с заказом. В чуть более сложных стейтмашинах/бизнес-логике это помогает сильнее, чем может показаться на первый взгляд: часто вообще тупо из типов становится понятно, что делать, и подход «подумал над предметной областью ⇒ записал типы ⇒ выключил мозг и пишешь реализации» приводит к успеху.

В коде явно указано, что finalizeOrder поставит только статус Finalized и никакой другой.

Для того, чтобы сделать такой вывод из ООП-кода, мне надо читать весь код, включая детали реализации вроде «а какая тема письма, отправляемого пользователю, с уведомлением об отправке заказа». Чтобы сделать такой вывод в типизированном ФП, вы просто читаете типы функций, всё остальное проверяет компилятор.

Это примерно как если бы JS-разработчик из середины нулевых спросил «а зачем вам в функции объявлять, что она принимает только инт, и количество аргументов указывать? видно же из кода».

И с типами точно так же, берем и делаем метод setFinalized :: Order Shipped → Order Finalized

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

Вот я бы хотел, чтобы человек показал этот вариант хотя бы самому себе.

Пишем что-то про заказы.

У заказов есть состояния: составляется, закончил составляться (финализирован), оплачен, отправлен. Во всех состояниях у заказа есть адрес отправки и назначенные товары. В состояниях «оплачен» и «отправлен» у заказов ещё есть адрес выставления счёта, во всех прочих состояниях он просто бессмысленен.

Заказ создаётся в состоянии «составляется». В этом же состоянии (и только в нём) в него можно добавлять товары. Из этого же состояния его можно финализировать. После того, как заказ финализирован, его можно оплатить (и вы либо получите ошибку оплаты, либо заказ перейдёт в состояние «оплачен»). После того, как заказ оплачен, его можно отправить покупателю. Никаких других переходов в системе нет.

Но да, очевидно можно, если приведённый код делает что-либо осмысленное

Я написал только типы, потому что всё самое интересное в типах. Реализации функций тут очень скучные, вроде

createOrder addr = Order addr [] ()

addItems items Order{..} = Order{.., goods = goods <> items }

finalizeOrder Order{..} = Order{..}

Так что, можно теперь ООП-вариант?

В неискусственных задачах лучше выходит. Вот, например, кусок NFA-интерпретатора с иммутабельными векторами:

match :: StateId q => NFA q -> BS.ByteString -> MatchResult Int
match NFA{..} bs = go initState 0 mempty
  where
  go q i stack
    | q == finState = SuccessAt i
    | otherwise = case q `getTrans` transitions of
             TEps q' -> go q' i stack
             TBranch q1 q2 -> go q1 i (stack `V.snoc` (q2, i))
             TCh ch q'
               | bs `BS.indexMaybe` i == Just ch -> go q' (i + 1) stack
               | Just (stack'', (q'', i'')) <- V.unsnoc stack -> go q'' i'' stack''
               | otherwise -> Failure

Вот он же с мутабельными линейными:

match :: forall q. StateId q => NFA q -> BS.ByteString -> MatchResult Int
match NFA{..} bs = L.unur L.$ VL.empty L.$ go initState 0
  where
  go :: q -> Int -> VL.Vector (q, Int) %1-> L.Ur (MatchResult Int)
  go q i stack
    | q == finState = stack `L.lseq` L.Ur (SuccessAt i)
    | otherwise = case q `getTrans` transitions of
                    TEps q' -> go q' i stack
                    TBranch q1 q2 -> go q1 i L.$ (q2, i) `VL.push` stack
                    TCh ch q'
                      | bs `BS.indexMaybe` i == Just ch -> go q' (i + 1) stack
                      | otherwise -> case VL.pop stack of
                                      (L.Ur top, stack'')
                                        | (Just (q'', i'')) <- top -> go q'' i'' stack''
                                        | otherwise -> stack'' `L.lseq` L.Ur Failure

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

Страуструп — в первую очередь учитель, поэтому задачи и проблемы, которые он видит — это чтобы «ученикам» можно было легко и просто показывать примеры простого, безбажного кода (и поэтому его характерная реакция сводится к «правильно показывайте пишите, неправильно не показывайте пишите»). Добавили там какой-нибудь std::span — всё, его проблема решена, потому что в учебных примерах можно им пользоваться (а плохим не пользоваться). То, что в продакшен-коде есть легаси, которое этим не пользуется, есть люди с разным бекграундом, с разным состоянием выспанности, в конце концов — это всё неважно. Страуструп смотрит на наличие способов не выстрелить себе в ногу, а не на отсутствие способов выстрелить.

Все эти сильные стороны неважны и не исправляют ситуацию, покуда есть слабые.

поэтому важно улучшать инструменты и процессы разработки

Самый лучший вариант в моём опыте — улучшить инструмент разработки через смену ЯП.

data State
  = Building
  | Finalized
  | Paid
  | Shipped

BillingAddressType :: State → Type
BillingAddressType Paid = BillingAddress
BillingAddressType Shipped = BillingAddress
BillingAddress _ = ()

data Order state = Order
  { shippingAddress :: ShippingAddress
  , goods :: [Item]
  , billingAddress :: BillingAddressType state
  }

createOrder :: ShippingAddress → Order Building

addItems :: [Item] → Order Building → Order Building

finalizeOrder :: Order Building → Order Finalized

payOrder :: PaymentInfo → Order Finalized → Either PaymentError (Order Paid)

shipOrder :: Order Paid → Order Shipped

Можно набросать соответствующую стейтмашину на чистом ООП?

(это было в ответ на

fsm и ООП никоим образом не противоречат друг другу, а наоборот, очень гармонично взаимно дополняют

а замечательный новый редактор комментариев не даёт добавить ньюлайны в начало комментария с кодовым листингом, по крайней мере, в FF — веб, который мы заслужили)

Людей, способных заниматься сексом, стоя в гамаке и на сноуборде тоже мало, и что за business value в этой наркомании с выкрутасами?

Это всё происходит во время компиляции, поэтому компилятор лучше оптимизирует то, что получается, поэтому перф выходит сильно лучше, чем если писать «просто». В некоторых задачах это ценят куда больше, чем поддерживаемость кода широким кругом лиц (тогда б изначально на плюсах не писали).

Можно просто сложный язык и/или сложную предметную область.
Современный C++ — сложный. Людей, могущих в выкрутасы на темплейтах и оптимизированный код, мало.
Разрабатывать свои языки — сложно. Людей, могущих сформулировать систему типов и доказать её хорошие свойства (или найти в ней проблемы), мало.

Во всех этих случаях не обязательно быть совсем уж ассенизатором-специалистом по легаси.

Иосиф Виссарионович легко посадит авиаконструктора, даже если в стране всего лишь 3 авиа КБ.

А кто посадит Иосифа Виссарионовича, когда тот начнёт зарываться?

Кто проверит, что его посадки действительно делают лучше, а не хуже? А то там, знаешь, потом было всякое, пересмотры, реабилитации…

И Зубину с Гамари легко намекнёт, что не надо ломать bootstrapp промышленного компилятора.

Ты в этом проблему видишь, я — нет. Что с этим делать будем? Сажать, если проигнорируют намёки, или нет?

Рынок ит за границей очень сильно поменялся за 2 года.

Причём тут два года? Это с ближайшей осени, несколько месяцев назад.

Но реально эти деньги никто не поставит в офер)))

А если поставить больше смайликов, то это будет ещё истиннее и универсальнее.

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

Будут прогибать, и прогнут очень сильно....ибо рынок работодателя.

Опять же, смотря на какие позиции. В интересных мне позициях (HFT-клоунада по верхней грани описанных в моём изначальном комментарии зарплат на руки или разработка компиляторов, тайпчекеров, и так далее, по нижней) как-то и не прогибают, и рынок едва ли работодателя.

Как мне хороший знакомый после 6 месяцев поиска сказал - я с позиции тимлида гугла (откуда его уволили после 4 лет работы ибо все направление закрыли а общий стаж в ит 17 лет) ушел на позицию синьера разраба в теслу на деньги мидла.

Гугл, наверное, призван был произвести впечатление, но я нему отношусь резко негативно что по уровню тамошних спецов, что по некоторым другим причинам, поэтому, ну, ок, ушёл и ушёл. Бывает. Люди вон на интервью тоже всякое говорят.

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

Не обязательно. Просто отправляете инвойс в фирму каждый месяц, и всё.

Учитывая разницу в налогах, денег будет даже больше (по крайней мере, это мой опыт работы не на W2, находясь изнутри США — там, во-первых, даже I-9 не спрашивают, а, во-вторых, эффективная налоговая ставка становится меньше 10%, и работодателю это выгодно, потому что он не должен заморачиваться со своей половиной FICA/SS/чётам).

Это не социализм, который в любом существующему на данный момент варианте отлично работает с монополиями (например в стиле «будете выпендриваться — посадим верхушку»).

Кто именно эти самые «мы», которые «посадим», в случае, когда соцгосударство и есть корпорация-монополист в худшем смысле этого слова?

Да. конечно. Вот хаскель:

import Data.Unrestricted.Linear qualified as L
import Data.Vector.Mutable.Linear qualified as VL
import Prelude.Linear qualified as L

fillVec :: Int → VL.Vector Int %1→ VL.Vector Int
fillVec n = go 0
  where
  go :: Int → VL.Vector Int %1→ VL.Vector Int
  go v vec
    | v == n = vec
    | otherwise = go (v + 1) L.$ VL.push (v + 1) vec

swap :: Int → Int → VL.Vector a %1→ VL.Vector a
swap p1 p2 vec =
  let %1 (L.Ur v1, vec¹) = VL.get p1 vec
      %1 (L.Ur v2, vec²) = VL.get p2 vec¹
   in VL.set p1 v2 L.$ VL.set p2 v1 vec² 

main :: IO ()
main = do
  let L.Ur vec = VL.empty (VL.freeze L.. swap 0 (n - 1) L.. fillVec n)
  print vec
  where n = 5
> :main
[5,2,3,4,1]

Или идрис:

import Data.Linear
import Data.Linear.Array

run : Int -> IO ()
run size = newArray size (\1 arr : _ => toIArray (swap 0 (size - 1) $ fill 0 arr) printArr)
  where
  fill : Int -> LinArray Int -@ LinArray Int
  fill n arr = if n == size
                  then arr
                  else let _ # arr = write arr n (n + 1) in fill (n + 1) arr

  swap : Int -> Int -> LinArray Int -@ LinArray Int
  swap p1 p2 arr =
    let mv1 # arr = mread arr p1
        mv2 # arr = mread arr p2
     in case (mv1, mv2) of
             (Just v1, Just v2) => let _ # arr = write arr p1 v2
                                       _ # arr = write arr p2 v1
                                    in arr
             _ => arr

  printArr : IArray Int -> IO ()
  printArr arr = for_ [0..size - 1] $ \i => case read arr i of
                                                 Just v => printLn v
                                                 Nothing => pure ()
Main> :exec (run 5)
5
2
3
4
1

т.е. всё-таки мы получаем ссылку на другую область памяти, а не изменяем данные in-place?

Нет, почему? Область памяти та же, просто имя другое.

Условно, в хаскеле с линейными типами операция записи в массив выглядит как

write :: Int → a → Array a %1→ Array a

где вот это вот %1→ означает, что переданным именем после вызова этой функции пользоваться нельзя, и надо пользоваться тем, что функция вернула (несмотря на то, что и старое, и новое имя ссылаются на один и тот же адрес).

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

read :: Int → Array a %1→ (a, Array a)

и старым именем пользоваться нельзя, хотя чтение-то уж точно ничего не копирует.

Информация

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