За долгое время поставил автору плюс за статью. Наконец то не про "боль и страдания короля", а про что-то интересное с технической точки зрения.
Да, интересно и раскрыта объемная тема в простых словах. Но для меня главный вопрос — куда делась предыдущая статься про F#?
Это навело меня на мысль, что весь вопрос в комбинации этих параметров.
Угу. Хотя можно представить себе язык, отдающий эти решения программисту:
type A = { name: string } // структурный тип, вместо него можно передать экземпляр B
nominal type B = { name: string } // номинальный тип, вместо него нельзя передать A
Насколько программист будет рад каждый раз делать этот выбор — неясно.
Анонимные рекорды в F# пытаются играть по примерно таким же правилам, кстати.
Уточню. В окамле несколько концептуально разных механизмов, при этом заметно пересекающихся по возможностям. Есть записи (records), и они номинативно типизированы, несмотря на то что выглядят как структуры: https://ideone.com/8mQqs7. Есть объекты с методами, и они типизированы структурно: https://ideone.com/GS3bVF. А ещё есть модули, у которых есть свои вроде-типы (сигнатуры) и вроде-функции (функторы), и они тоже типизированы структурно: https://ideone.com/TBiz1F.
К слову говоря, Python позволяет так делать. Можно использовать аналог номинального типа Protocol, который проверяет объект на соответствие методам, атрибутам. А можно использовать обычный класс или тип, который ведёт себя классическим образом.
При чём доступ к этим типам есть в рантайме, многие либы уже их используют, но основное предназначение статический анализ кода и подсказки в IDE.
Но вообще спасибо за статью, понял глубину задумки аннотации типов Python. Какая мощная штука оказалась.
Вот F# — номинативно типизированный ЯП. Но. Но. В нем можно написать функцию, которая работает с чем угодно, у чего есть такое-то поле, такого-то типа.
а чем вам generic классы не угодили?
Но где написано, что это противоречит? :)
в C# нельзя сделать вторую часть, а именно структурную типизацию(наличие поля/метода).
Я одно время даже думал из-за этого писать на F# немного, так как там я могу написать универсальный тип Vector.
Ну грубо говоря, есть пару сахаров, которые применяют подобный подход.
Но, все же, это все строго регламентировано, да и не позволяет писать гибких генериков.
Вон ниже, как раз пример привели
public class A : IA
{
public string id { get; set; }
}
public class B : IA
{
public string id { get; set; }
public string name { get; set; }
}
public interface IA
{
public string a { get; set; }
}
public void test(IA a) {}
1)Туда входят только методы и свойства, поля и статические методы(в том числе операторы) не могут входить.
2)В сторонний класс реализацию интерфейса не запихнуть.
Этот комментарий показывает примеры чего шарп не может.
У С# же есть интерефейсы. Разве нельзя ограничивать по ним?
Тут принципиальна не неявность реализации интерфейса, а отделение описания типа от реализации интерфейса для типа. И Go в этом плане не оригинален: такое же разделение, но с явной реализацией, есть в Rust, Swift, Scala, Ocaml, Haskell и древнем StandardML — и это только те, о которых мне известно.
Что-то вроде такой ситуации?
interface Shape {
int area();
}
class Square: Shape { // тут интерфейс
int area(){ /*impl*/}
}
class Circle { // а тут нет интерфейса
int area(){ /*impl*/}
}
Да, при этом class Circle
у вас где-то в libcircles.dll
и запатчить туда свой интерфейс нет возможности.
а чем вам generic классы не угодили?
тем что в C# нельзя написать такой метод
static T Sum<T>(T x, T y): where T: operator (+) => x + y;
который бы работал для любых типов, у которых объявлен статический оператор сложения.
А в F# можно
Это не только про операторы.
например такое возможно в фшарпе.
Функция, которая достаёт из любого типа поле Id без навешивания на тип интерфейсов.
static string GetId<T>(T entity)`
: where T: string Id { get; } =>
entity.Id;
Это можно сразу исключить. Рефлексия не является zero-cost abstraction и она не проверяется статически.
В фшарпе такая функция неотличима в коде от вызова проперти (то есть zero-cost) и в неё нельзя сунуть тип, у которого нет такого свойства (т.к. это ограничение на тип).
Код, который пользуется операторами, я пишу часто. Вспомните: те же DateTime и TimeSpan перегружают арифметические операторы.
См. ниже. При желании всегда можно написать руками, и выглядит не очень ужасно. Что до автоматического генерирования структурных тайпклассов — ну… Не знаю, насколько это хорошая идея. Мне кажется, что это не очень полезно.
Есть классический способ энкодить тайпклассы в сишарпе:
void Main()
{
var numbers = new[] { 1, 2, 3, 4, 5 };
var strings = new[] {"Hello", " ", "World" , "!"};
Console.WriteLine(MConcat<int, IntSGroup>(numbers));
Console.WriteLine(MConcat<string, StringSGroup>(strings));
}
public interface SGroup<T> // наш тайпкласс
{
public T Zero { get; }
public T Add(T x, T y);
}
public struct IntSGroup : SGroup<int> // реализации для наших типов - привет, имплиситы
{
public int Zero => 0;
public int Add(int x, int y) => x + y;
}
public struct StringSGroup : SGroup<String>
{
public String Zero => "";
public string Add(string x, string y) => x + y;
}
// пример абстрактного кода, работающего с тайпклассами
public static T MConcat<T, TGroup>(IEnumerable<T> items) where TGroup : struct, SGroup<T> {
var typeclass = default(TGroup);
return items.Aggregate(typeclass.Zero, typeclass.Add);
}
Его, собственно, и планируется реализовать в пропозале тайпклассов, только этим компилятор заниматься будет. Но и щас руками можно такое делать. Я даже делаю в паре мест, где это полезно, правда, не все ценят.
Я не скажу за все япы, но все, на которых писал код я, иногда делают такие касты. То есть я не знаю ни одного абсолютно сильно типизированного языка программирования, и поэтому, когда мы говорим "слаботипизированный" — мы имеем в виду такой, который делает такие касты до неприличия часто.
Хаскель не делает, например. Что интересно, куда более строгий идрис позволяет зарегистрировать функции, которые будут использоваться для кастов.
Впрочем, это неважно. По крайней мере, с моей точки зрения сила типизации не в кастах, а в том, сколько у вас возможностей убедиться, что в рантайме не будет нежелательного поведения (и кидание экзепшона от некорректного каста — лишь одно из таких поведений). В условном С вы никак не можете статически гарантировать, что обращение по указателю имеет смысл, и указатель не висячий, например, и это тоже признак слабой системы типов.
Моего понимания не хватает, что бы сказать, нужно ли тащить метаданные типов в рантайм.
Нет, конечно. Чем меньше притащено в рантайм, тем быстрее всё работает. Да и если вы типы проверили, то зачем вам эта информация в рантайме?
Но при этом неплохо иметь способ попросить компилятор сгенерировать информацию о типах, да, когда это изредка бывает нужно (как тот же Typeable в хаскеле).
Вот F# — номинативно типизированный ЯП. Но. Но. В нем можно написать функцию, которая работает с чем угодно, у чего есть такое-то поле, такого-то типа. Более того, я могу весь свой код писать в таком стиле — это будет структурно типизированный код на номинативно типизированном F#. Как с этим быть?
Есть такая тема, как row polymorphism, и это очень клёвая тема. Не знаю, есть ли она в F# (я не знаю F#), но в этом несчастном хаскеле её очень не хватает.
Нет, конечно. Чем меньше притащено в рантайм, тем быстрее всё работает. Да и если вы типы проверили, то зачем вам эта информация в рантайме?
Тащить их всегда — да, наверное не нужно. Но они ведь не только для проверок используются.
Например паттерн матчинг по типам — он же не заработает без метаинформации. Понятно, что компилятор может протащить инфу только для тех типов, на которых используется этот паттерн матичнг, и наверное, это и есть правильный путь
Так для паттерн-матчинга про типы ничего знать не обязательно, достаточно одного int
а для различения разных конструкторов.
https://en.wikipedia.org/wiki/Tagged_union#1970s_&_1980s
enum ShapeKind { Square, Rectangle, Circle };
struct Shape {
int centerx;
int centery;
enum ShapeKind kind;
union {
struct { int side; }; /* Square */
struct { int length, height; }; /* Rectangle */
struct { int radius; }; /* Circle */
};
};
Насколько я понимаю, хаскеллевские алгебраические типы компилируются во что-то подобное. 0xd34df00d, вероятно, не считает kind информацией о типе
Стоп, вы про паттен-матчинг по типам? А в каком языке (кроме Idris 2) вы это можете делать? В хаскеле по типам вы матчиться не можете.
А я, если что, говорил о
data Foo = F1 | F2 Int | F3 Bool
data Bar = B1 Double | B2 Foo
f1 :: Foo -> Bool
f1 F1 = ...
f1 ...
f2 :: Bar -> Int
f2 B1 = ...
f2 ...
Обе функции могут смотреть в условный int
по адресу, на который указывает аргумент. Для первой функции значение 0
будет означать F1
, а для второй то же самое значение может означать B1
. Где ж тут информация о типе? Она тут только о номере конструктора.
А в каком языке (кроме Idris 2) вы это можете делать?
C#
А, ну там что угодно может быть. А, кстати, как там паттерн-матчинг по типам выглядит?
public static double ComputeAreaModernSwitch(object shape)
{
switch (shape)
{
case Square s:
return s.Side * s.Side;
case Circle c:
return c.Radius * c.Radius * Math.PI;
case Rectangle r:
return r.Height * r.Length;
default:
throw new ArgumentException(
message: "shape is not a recognized shape",
paramName: nameof(shape));
}
}
Пример отсюда
Любопытно, спасибо!
Ну, тогда уточню, что я говорил об ФП-языках (что тоже размытое понятие, конечно, но, надеюсь, интуитивно понятное). ФП-языки очень боятся давать возможность делать паттерн-матчинг по типам, потому что это ломает параметричность, а сломанная параметричность ломает всякие полезные теоремы о внутренней логике языка (но там уже матан за пределами моих знаний).
Но в случае, например, Union-типов, когда на входе у нас, например, `
Customer | Error
`, можно обработать ошибочный сценарий в функциональном стиле при помощи паттерн-матчинга. Эх, вот бы наконец завезли в C# Union-типы…type A = B of int | C of int
и собственно вот этот размеченный лейбл и едет в рантайм. И по нему же, наверное, паттерн матчинг и работает
В Fortran, впрочем, были COMMON-блоки, которые позволяли калбмур типизации делать.
А потом — случилась революция.
Движение в сторону строгости и меньшего числа ошибок, фактически, прекратилось — и вместо этого задачу борьбы с ошибками возложили почти всю на программиста.
А сейчас — вообще период двоемыслия. Когда все менеджеры «бьют себя пяткой в грудь» и публично объясняют что борьба с ошибками — это очень важно и нужно… а потом устраивают общения с разработчиками один-на-один и пытаются сделать так, чтобы «таски» как можно быстрее закрывались. «А если ошибки возникнут — так мы их потом пофиксим».
Эх, вот бы наконец завезли в C# Union-типы…
Переходите на PHP — к нам завозят в этом году :)
ФП-языки очень боятся давать возможность делать паттерн-матчинг по типам, потому что это ломает параметричность
Нет, не поэтому. Просто для паттерн-матчинга по типам нужно для начала ввести в язык подтипы и полиморфизм подтипов.
А параметричность вы сами ломали когда делали Has на дженериках, и никто из-за этого не умер...
Нет, не поэтому. Просто для паттерн-матчинга по типам нужно для начала ввести в язык подтипы и полиморфизм подтипов.
Вовсе нет:
notId : {a : Type} -> a -> a
notId {a = Nat} _ = 0
notId {a = Bool} b = not b
notId n = n
даёт
% idris2
____ __ _ ___
/ _/___/ /____(_)____ |__ \
/ // __ / ___/ / ___/ __/ / Version 0.2.0-4aa1c1f44
_/ // /_/ / / / (__ ) / __/ https://www.idris-lang.org
/___/\__,_/_/ /_/____/ /____/ Type :? for help
Welcome to Idris 2. Enjoy yourself!
Main> :l Dyna.idr
1/1: Building Dyna (Dyna.idr)
Loaded file Dyna.idr
Main> notId (the Nat 10)
0
Main> notId False
True
Main> notId (the Int 10)
10
Main> notId "meh"
"meh"
Main> :total notId
Main.notId is total
Паттерн-матчинг? Да. По типам? Да. Без универсумов? Да.
А параметричность вы сами ломали когда делали Has на дженериках, и никто из-за этого не умер...
Эм, почему же? Это всего лишь рефлексия. Рантайм-поведение не отличается от того, как если бы я писал все нужные инстансы руками.
Ну, я писал про паттерн-матчинг переменной по типу, а не типа по типу. Если начать считать вторые случаи — то в список языков с паттерн-матчингом по типам надо внести С++ (специализация шаблонов работает как паттерн-матчинг), Rust (реализации трейтов) и Haskell (инстансы тайпклассов, ограниченно). Даже странно, что вы не припомнили ни одного из этих языков :-)
Эм, почему же? Это всего лишь рефлексия. Рантайм-поведение не отличается от того, как если бы я писал все нужные инстансы руками.
Отличается. Если я ничего не перепутал, то можно взять для примера вот такую функцию:
foo :: a => (a, Bar) -> Bar
foo = extract
Так вот, для любого a
кроме некоторых странных случаев она будет возвращать второй элемент пары, но для пары (Bar, Bar)
она вернёт первый. И это далеко не все возможные варианты...
Если начать считать вторые случаи — то в список языков с паттерн-матчингом по типам надо внести С++ (специализация шаблонов работает как паттерн-матчинг)
Ну C++ не ориентируется на целостность и цельность с точки зрения всяких лямбда-кубов, про него лучше вообще забыть (по крайней мере, в таких дискуссиях).
Rust (реализации трейтов) и Haskell (инстансы тайпклассов, ограниченно).
Про раст я знаю недостаточно, а хаскель с тайпклассами — так эти тайпклассы — это всего лишь ещё один параметр функции. Когда вы пишете foo :: TypeClass tc => ...
, то вы на самом деле неявно имеете у функции ещё один параметр со словарём функций для tc
, и всё. Внутри функции при этом вы паттерн-матчиться на конкретный инстанс тайпкласса не можете.
Так вот, для любого a кроме некоторых странных случаев она будет возвращать второй элемент пары, но для пары (Bar, Bar) она вернёт первый.
Более того, можно проще без всяких дженериков (пишу из головы, код может не компилироваться, и его надо посыпать всякими там OVERLAPPING
, но идея понятна, надеюсь):
class Stupid a where
stupidId :: a -> a
instance Stupid Bool where
stupidId = not
instance Stupid a where
stupidId = id
Но тогда вы всю непараметричность запихиваете в выбор словаря для инстанса Stupid
. А этот выбор в итоге в том или ином виде происходит статически (потому что конкретный тип в какой-то точке известен статически), и вы могли бы аналогично без тайпклассов передать нужную функцию в той самой точке.
Опять же, там матан за пределами моих знаний, но моя ментальная модель такая, что всё ломается, когда вы внутри функции можете сделать паттерн-матчинг по типу. Пришедший снаружи словарь с конкретным поведением конкретного типа — он снаружи и поэтому неважен.
Внутри функции при этом вы паттерн-матчиться на конкретный инстанс тайпкласса не можете.Погодите. Вот тут же написано что можете:
char x = case cast x of
Just (x :: Char) -> show x
Nothing -> "unknown"
Опять же, там матан за пределами моих знаний, но моя ментальная модель такая, что всё ломается, когда вы внутри функции можете сделать паттерн-матчинг по типу.Ментальная модель ломается — но код-то работает!
Typeable
— это ну такое. Во-первых, Typeable
— это ИМХО плохо пахнущий код. Во-вторых, ваш тип может не поддерживать Typeable
, если вы не написали deriving (Typeable)
. Во-третьих, вы делаете паттерн-матчинг не по типу, а по значению (Typeable
сводится к наличию рантайм-метки у объектов).
То есть, это эквивалентно классу вроде
class RuntimeId a where
getId :: proxy a -> Int
instance RuntimeId Char where
getId _ = 0
instance RuntimeId MyType where
getId _ = 100500 -- как-то гарантируем уникальность
и последующим вещам вроде
withRuntimeId :: RuntimeId a => a -> String
withRuntimeId _ =
case getId (Proxy :: Proxy a) of
0 -> "yay got int"
1 -> ...
100500 -> "yay got my type"
Кстати, интересно, что в кишках cast
использует unsafeCoerce
:
eqTypeRep :: forall k1 k2 (a :: k1) (b :: k2).
TypeRep a -> TypeRep b -> Maybe (a :~~: b)
eqTypeRep a b
| sameTypeRep a b = Just (unsafeCoerce# HRefl)
| otherwise = Nothing
а вот это вот sameTypeRep
, считайте, сравнивает эти самые метки.
Короче, снова функции и словари.
Но тогда вы всю непараметричность запихиваете в выбор словаря для инстанса Stupid. А этот выбор в итоге в том или ином виде происходит статически, и вы могли бы аналогично без тайпклассов передать нужную функцию в той самой точке.
Ха-ха:
notId :: a => a -> a
notId = stupidId
Ну да, вроде и статически реализация выбирается, но это не помогает: все "бесплатные теоремы" похерились точно так же, как могли похериться при паттерн-матчинге по типу.
Опять же, там матан за пределами моих знаний, но моя ментальная модель такая, что всё ломается, когда вы внутри функции можете сделать паттерн-матчинг по типу.
Параметричность ломается не только при паттерн-матчинге по типу, это всего лишь самый простой способ её сломать.
Ещё она ломается при:
- использовании Overlapping instances
- использовании Type families (именно так вами была сломана параметричность у extract)
В общем, она ломается при любом паттерн-матчинге по типу, вовсе не обязательно внутри функции.
Ха-ха:
Не имеете права. Надо написать notId :: Stupid a => a -> a
. И вот вам ваш словарь.
использовании Overlapping instances
С учётом того, что там таки везде нужен дополнительный параметр, описывающий поведение, не уверен.
использовании Type families (именно так вами была сломана параметричность у extract)
Это всего лишь вычисления на типах, как они могут повлиять на рантайм-поведение в этом смысле?
Не имеете права. Надо написать notId :: Stupid a => a -> a. И вот вам ваш словарь.
Неа, не надо. Инстанс instance Stupid a
даёт право не указывать такое ограничение, только что проверил.
Это всего лишь вычисления на типах, как они могут повлиять на рантайм-поведение в этом смысле?
Нарушают "регулярность" системы типов языка. Точно не скажу (теперь уже мне матана не хватает).
Неа, не надо. Инстанс instance Stupid a даёт право не указывать такое ограничение, только что проверил.
Да, вы правы.
Прикольно, лишний повод говорить о неконсистентности хаскеля.
Неа, не надо. Инстанс instance Stupid a даёт право не указывать такое ограничение, только что проверил.
Так, я тут делал что-то другое и вдруг внезапно вспомнил про этот пример и таки решил с ним поиграться вживую.
Как и интуитивно ожидалось, там надо включить IncoherentInstances
(на что в хаскель-сообществе смотрят очень косо, и лично для меня это почти всегда знак, что я делаю что-то сильно не то):
Такой код:
{-# LANGUAGE FlexibleInstances #-}
class Stupid a where
notId :: a -> a
instance Stupid a where
notId = id
instance Stupid Bool where
notId = not
stupidId :: a -> a
stupidId = notId
Без IncoherentInstances
:
• Overlapping instances for Stupid a arising from a use of ‘notId’
Matching instances:
instance Stupid a -- Defined at Main.hs:6:10
instance Stupid Bool -- Defined at Main.hs:9:10
(The choice depends on the instantiation of ‘a’
To pick the first instance above, use IncoherentInstances
when compiling the other instance declarations)
• In the expression: notId
In an equation for ‘stupidId’: stupidId = notId
|
13 | stupidId = notId
| ^^^^^
Интересно, что с констрейнтом Stupid a
оно акцептится (что разумно — выбор нужного инстанса делегируется вызывающему коду).
Нарушают "регулярность" системы типов языка. Точно не скажу (теперь уже мне матана не хватает).
Но ведь это всего лишь кусочек того, что дают завтипизированные языки. Мне неочевидно, что там нарушается.
Э-э-э, а как вы без IncoherentInstances будете использовать Overlapping instances?
Как использовать — да спокойно, делегируя выбор инстанса выше и используя {-# OVERLAPS #-}
,
{-# LANGUAGE FlexibleInstances #-}
class Stupid a where
notId :: a -> a
instance Stupid a where
notId = id
instance {-# OVERLAPS #-} Stupid Bool where
notId = not
stupidId :: Stupid a => a -> a
stupidId = notId
main :: IO ()
main = do
print $ stupidId True
print $ stupidId "meh"
Теперь констрейнт с stupidId
вы снять не можете, но main
допустим и ведёт себя как ожидалось (или не ожидалось, хз).
Впрочем, мне перестало быть очевидно, что так всё равно нельзя будет сломать параметричность или что-то рядом с ней
Поигрался ещё и, кажется, беру свои слова о том, что вы правы, назад.
Окей, вы включили IncoherentInstances
и написали stupid :: a -> a ; stupid = notId
. У меня не получилось добиться того, чтобы stupid True
было False
(что разумно — выбор инстанса происходит при компиляции stupidId
, а не при её инстанциировании, при её инстанциировании тело может быть вообще недоступно). Получилось ли у вас?
Получилось ли у вас?
Неа, я же не проверял как оно в рантайме.
Но если тут нарушается принцип "скомпилировалось -> работает" — это ж ещё хуже, разве нет?
Ну почему? Работает же. Выбирается инстанс для a
. Если помнить, что тайпчекинг идёт до мономорфизации, то это, наверное, даже выглядит логичным.
static string Display(object o)
{
return o switch
{
Point p when p.X == 0 && p.Y == 0 => "origin",
Point p => $"({p.X}, {p.Y})",
_ => "unknown"
};
}
Источник
Можно убрать return и фигурные скобочки
static string Display(object o) =>
o switch
{
Point p when p.X == 0 && p.Y == 0 => "origin",
Point p => $"({p.X}, {p.Y})",
_ => "unknown"
};
паттен-матчинг по типам?
А в каком языке (кроме Idris 2) вы это можете делать?
Go например.
не так изящно как сигнатурами функций, но вполне рабочий вариант
Перегрузка в сигнатурах функций — тоже неизящный, ad-hoc полиморфизм.
Стоп, вы про паттен-матчинг по типам? А в каком языке (кроме Idris 2) вы это можете делать?
Вообще говоря, формально, любой паттерн-матчинг является матчингом по типам. По крайней мере — его можно так рассматривать и нельзя сделать такой матчниг, который в виде матчинга по типам рассматривать было бы нельзя.
Можете развернуть идею?
Вам никто не мешает считать, что на самом деле у вас есть отдельный тип для каждого конструктора, а размеченное объединение выражается через простое объединение пар — это как бы ничего не сломает, если вы не можете к этим типам обращаться "напрямую" (а в хаскеле не можете). В этом случае любой матчинг — он по типам, т.к. для каждой ветки матчинга найдется свой тип.
а размеченное объединение выражается через простое объединение пар
Но я всё равно же матчусь по элементам t : T
, где T
— объединение. Из этого не следует, что я могу матчиться по любым элементам t : *
, где *
такой, что T : *
.
Но я всё равно же матчусь по элементам t: T, где T — объединение.
Или, что одно и то же, по типам Ai, где T = A1 U A2 U… U An. Нельзя отличить эти два кейза, это просто два описания одного и того же.
trait PatternProducer {
fn pattern(&self) -> &'static str;
}
struct Class {
producer_fn: Box<dyn Fn() -> &'static str>
}
impl Class {
pub fn new(pattern: &'static str) -> Self {
Class {
producer_fn: Box::new(move || pattern)
}
}
}
impl PatternProducer for Class {
fn pattern(&self) -> &'static str {
(self.producer_fn)()
}
}
fn print_pattern(class: &dyn PatternProducer) {
match class.pattern() {
"a" => println!("Match A"),
"b" => println!("Match B"),
_ => println!("Unknown pattern")
}
}
fn main() {
print_pattern(&Class::new("a"));
print_pattern(&Class::new("b"));
print_pattern(&Class::new("c"));
}
На самом деле, информация о типах — это такие же данные, как и все остальное.
На эти данные навешен какой-то функционал в рантайме, который поддерживается языком.
Если у Вас есть Тьюринг-полный язык, вы можете реализовать свою «типизацию». Обычно это легко сделать в рантайме, в компайл тайме не всегда.
- это не матчинг по типам
- это уже реализовано для вас в трейте Any.
2. Я не против
Матчинг по типам это когда я могу сделать:
fn foo<T>() -> String{
let ty = T;
match ty {
i32 -> "This is int",
f64 -> "This is double",
_ -> "This is something else";
}
}
Впрочем, матчиться непосредственно по типам (даже в языках которые теоретически это разрешают) все равно плохая идея.
По поводу второго утверждения, я крайне несогласен. Паттерн матчинг и ADT сильно добавляют мощности языку. По вашей же ссылке в комментарии с галочкой опровергают то, что матчится по типа — это плохо.
P.S. Я допускаю, что теоркатщики могут себе придумывать проблемы с параметризированностью типов, но на практике, паттерн матчинг делает код чище. Достаточно пописать на Typescript с его кастрированным паттерн матчингом и Rust/ML/Ваш вариант, и увидеть насколько все проще выглядит на практике. Я уже не говорю про С с union/enum парами.
Речь не про паттерн-матчинг, а матчинг по типам. Это нарушает параметричность, отсюда много всякого плохого следует.
Речь про то, что таким образом вы никак не поматчитесь по типам про которые ничего не знаете, да ещё и руками пишете.
В сишарпе, к слову, можно. Не считаю это плюсом.
все равно плохая идея
О, Коннор МакБрайд :)
Не знаю, этот его ответ был до его работы по QTT или после. Одно из следствий QTT — можно матчиться по типам, которые в сигнатуре функции аннотированы как runtime-relevant (или какое там правильное название). То есть, в каком-то смысле можно говорить, что вы поднимаете признак параметричности на уровень типов.
Я там ниже приводил пример функции, которая тайпчекается Idris 2 (который реализует QTT):
notId : {a : Type} -> a -> a
notId {a = Nat} _ = 0
notId {a = Bool} b = not b
notId n = n
Если убрать {a : Type}
из сигнатуры, то матчиться уже будет нельзя. Причём это всё работает точно так же, как и паттерн-матчинг по обычным значениям в случае, например,
doSmth : {n : Nat} -> Vect n a -> ...
В этой функции вы можете матчиться на n
. Но если вы уберёте {n : Nat}
, то всё, нельзя, это рантайм-иррелевантная информация.
О, Коннор МакБрайд :)
Круто, а я вот по никнеймам не угадываю что за люди передо мной, не так много всех знаю.
Я там ниже приводил пример функции, которая тайпчекается Idris 2 (который реализует QTT):
Я пока на первом сижу, мб к концу года перейду на 2 (но скорее всего — нет, т.к. после книги по идрису начну читать что-то про ремонт :) )
В этой функции вы можете матчиться на n. Но если вы уберёте {n: Nat}, то всё, нельзя, это рантайм-иррелевантная информация.
Прикольно, да. И не надо туда-сюда конвертеры писать, и параметричность вроде сохранили.
Круто, а я вот по никнеймам не угадываю что за люди передо мной, не так много всех знаю.
Это один из полутора ников, которые я хорошо запомнил. «pigworker» звучит смешно, и для работающего в академии человека это довольно нетипичный ник.
Я пока на первом сижу, мб к концу года перейду на 2 (но скорее всего — нет, т.к. после книги по идрису начну читать что-то про ремонт :) )
Переходить пока рано, он даже к развлекательному программированию ещё не особо готов. Я-то его ковыряю отчасти потому, что интересна QTT, отчасти — потому что кое-какие вещи не тайпчекаются в Idris 1, отчасти — планирую в скором будущем поковырять кишки тайпчекера, а для первого идриса это делать глупо, так как он мёртв.
А этот инт логически не будет информацией о типе?
Нет, про тип он ничего не знает.
Насколько я могу судить по беглому гуглу и примерам (так как не знаю Elm) — похоже, да.
Есть такая тема, как row polymorphism, и это очень клёвая тема. Не знаю, есть ли она в F# (я не знаю F#), но в этом несчастном хаскеле её очень не хватает.Расскажите, пожалуйста, почему raw polymorphism не хватает? Часто встричаю утверждение что его не хватает в Haskell, но почему не объясняется. Разница между ad-hoc и raw в моём текущем представление только в том что в первом нужно генерировать больше бинароного кода, во втором нужно делать виртуальные таблицы методов, и как следствие делать некую рефлексию, при этом не понятно какие возможности по верх добавит второй?
Динамическая диспетчеризация (если я верно понял суть row polymorphism) обычно используются там, где писать ad-hoc реализацию слишком многословно или когда tagged union построить невозможно ввиду особенностей архитектуры проекта. Или там, где система типов не очень богатая (Java, C# и пр.).
Не уверен, насколько это применимо к Haskell, сужу по своему опыту кодирования на Rust. Частенько бывает что, отделяя абстракцию от реализации, разработчик получает ситуацию, когда итоговый union тип становится возможно построить лишь в проекте самого верхнего уровня иерархии. И чтобы обеспечить его использование, нужно либо обмазываться полиморфизмом сильнее (что весомо усложняет код), либо использовать динамическую диспетчеризацию.
А как вы с каноничным ad-hoc (в смысле наличия перегрузки функций) напишете функцию, которая работает с любым рекордом, в котором есть поля foo :: Int
и bar :: Double
?
Зато это можно гарантировать в ATS [1] [2], который скомпилится в нужный C и всё будет безопасно и быстро [3] :)
[1] ats-lang.sourceforge.net/htdocs-old/TUTORIAL/contents/tutorial_all.html#pointers
[2] bluishcoder.co.nz/2013/01/25/an-introduction-to-pointers-in-ats.html
[3] blog.vmchale.com/article/ats-performance
Хотя, в шарпе всё же есть интересные вещи похожие на структурную типизацию. Например метод Dispose.
вероятно явную динамическую типизацию причисляют к статике.
В шарпе все же надо явно объявить dynamic, а не оно само.
Вначале дискуссия крутилась вокруг внутренней реализации (типа если 6502 увеличивает счётчик команд за два такта, «в два прохода» — то это однознано 8-битный процессор, а если 65816 — этого не требует, то это уже 16-битный процессор), но потом, разумеется, Prescott все карты спутал.
В итоге сошлись на том, что если люди называют какой-то процессор 8-битным — то это 8-битный процессор, а если 64-битным, то 64-битный…
Ну зашибись просто! А это, вроде как, просто о чиселке идёт речь! Его, вроде как, можно бы как-то из наблюдаемой под микроскопом картинки извлечь!
А вот нифига.
А вы хотите, чтобы вам чёткие критерии для классификации языков кто-то дал…
А разве битность процессора — это не про адресацию? Т.е. размер указателя на объект из кучи.
Да даже и у современного x86-64 ведь адрес 48-битный (до Ice Lake, у этих он, прости господи, 57-битный).
Чем, кстати, некоторые нехорошие люди пользуются.
Собственно беда-то вся та же, что и с типизацией: мы хотим одним словом охватить несколько разных объектов (размер РОН'а, размер адресной шины, размер шины данных и так далее).
А в реальном мире эти вещи, внезапно, имеют разный размер. И разные люди, в своих классификациях, выбирают «самым важным» разные вещи…
Если под гибкостью имеется в виду необходимость следовать НЕЯВНЫМ контрактам, то такая гибкость — нафиг не нужна. А нужна возможность извне указать, что один тип совместим с другим типом. Мэдс что то такое для шарпа тут https://youtu.be/WBos6gH-Opk?t=2757 описывает.
Нет, нельзя. Интерфейсы в C# — такие же номинативные типы.
struct A {
int x;
};
struct B {
int x;
};
int foo(A a) {
return a.x + 3;
}
int foo(B b) {
return b.x * 3;
}
auto result1 = foo(A{3}); // 6
auto result2 = foo(B{3}); // 9
С другой стороны такое вот:auto result3 = foo(*static_cast<B*>(static_cast<void*>(&A{3}))); // 9
законно тоже.И? Это номинативные типы или уже структурные?
Не вижу в вашем примере эмуляции структурной типизации через интерфейсы.
Теоретически, мы можем научить компилятор тайпскрипта генерить нам рантайм проверки для определенных типов, и не тащить метаданные всех типов в рантайм — это было бы даже круче. но команда тайпскрипта следует своей философии не влиять на рантайм, и никогда на это не пойдет.
А еще есть compile time рефлексия или процедурные макросы, которые могут преобразовывать AST на этапе компиляции.
Это часто используется в языках типа Хаскеля и Раста, туда же можно записать лиспомакросы.
Вообще, мне кажется, что тут просто все можно свести в таблицу и по ней строить классификации языков.
Посмотрите typescript-is, хорошая библиотека для рантайм-проверок на основе TS-интерфейсов.
Проблема в том, что есть много случаев, когда мне в коде требуются именно номинативные проверки. Например, у меня есть одинаковые по структуре сущности Customer и Client, и я хочу написать метод, который сохраняет клиента в базу.
Вроде есть некий костыль, который позволяет это делать в TypeScript, некоторые его ещё называют "branded types"
https://medium.com/better-programming/nominal-typescript-eee36e9432d2
оу, да, прошу прощения, пропустил этот абзац)
Моего понимания не хватает, что бы сказать, нужно ли тащить метаданные типов в рантайм. Я знаю, что тайпскрипт этого не делает, и я решал проблемы, которые из-за этого возникают. Сишарп это делает, и это тоже вызывает проблемы — но я не знаю, были бы у меня такие проблемы, если бы сишарп при этом был структурно типизированным.
Scala позволяет оба варианта, и мне это кажется близким к идеалу. По-умолчанию типы дженериков стираются, но при необходимости можно указать, что требуется ClassTag, и компилятор его предоставит.
Есть языки программирования, которые потащат в рантайм метаинформацию этого типа.
Думаю, это называется рефлексией
Получается, что если я описал тип User, и получил список таких юзеров из JSON, мне придётся руками перечислять все свойства, и делать все проверки. Это объективно — говно.
Теоретически, мы можем научить компилятор тайпскрипта генерить нам рантайм проверки для определенных типов, и не тащить метаданные всех типов в рантайм — это было бы даже круче.
Не уверен на 100%, но кажется эта статья может понять куда дальше двигаться, если хочется прокопать это направление
http://blog.wolksoftware.com/decorators-metadata-reflection-in-typescript-from-novice-to-expert-part-4
Нет, не называется. Метаинформацию в рантайме в C++ вы получить можете (если RTTI не выключите), а вот рефлексии там нет. Хотя есть Magic Get, позволяющий узнать какие у структуры поля и каких типов и Magic Enum, позволяющий узнать названия элементовЕсть языки программирования, которые потащат в рантайм метаинформацию этого типа.Думаю, это называется рефлексией
Enum
.При этом то же самое, но уже без хитрых трюков — собираются добавить в C++23… и это уже будет называться рефлексией…
Нет, не называется. Метаинформацию в рантайме в C++ вы получить можете (если RTTI не выключите), а вот рефлексии там нет.
Хм, интересно… А что ещё из обязательного включается в понятие рефлексии, что получение метаинформации о типах через RTTI нельзя ей назвать?
Вот наблюдать за чем-то можно (но очень ограниченно), а вот «модифицировать себя» — тут напряг. Даже в компайл-тайм нельзя, не говоря про рантайм. Вся поддержка рефлексии, фактически, нужна в C++98, чтобы
dynamic_cast
работал. Это, как бы сказать, очень ограниченный вариант.Насколько я понял из https://en.wikipedia.org/wiki/Type_theory нет какой-то единственной и универсальной системы типов, а соответственно и сгруппированных по этой классификации языков программирования
Еще о двух вещах хочу добавить, которые позволяют в метапрограммирование — это наличие обобщений и макросов.
Причем обобщения бывают как шаблоны в плюсах, где проверка производится после инстанцирования (по сути дела они обладают той самой структурной типизацией) и где проверки производятся до инстанцирования и таким образом на типы в обобщения нужно накладывать ограничения в виде интерфейсов или трейтов. Помимо этого есть еще и такие несчастные случаи, когда информация о типах стирается и во время runtime рефлексии невозможно отличить
List<String>
List<Int>
друг от друга (привет Java).
Опять же, что касается интерфейсов, то помимо традиционной схемы с наследованием есть языки, где можно реализовывать интерфейсы для произвольных типов, в том числе чужих. Привет Haskell type class.
Но некоторым языкам и такого метапрограммирования мало, они вводят еще и макросы, которые бывают как гигиеническими, так и нет.
Получается, что в такой вот общей классификации было бы ну дохрена различных пунктов.
Интересно почему метод падал на Customer и Client, если они структурно одинаковые?
Ну а так заценил преимущество структурной типизации работая с graphql.
Долго пытался протаскивать типы из кверей, а потом понял, что если я в UI компоненте отображаю id, name и value — то надо просто написать что этому UI компоненту нужна сущность с id, name и value — а уже тайпскрипт проверит, что туда спускается из квери соответствующий объект.
Интересно почему метод падал на Customer и Client, если они структурно одинаковые?
Имел ввиду, что метод как раз не упал — произошёл более плохой вариант, когда у нас сущность не в ту таблицу записалась
Ну кстати из структурной типизации при помощи всяких хитростей можно сделать номинативную, а наоборот уже нет.
Номинальная и структурная типизация могут уживаться в одном языке. Например во flow вы можете задавать их явным образом https://flow.org/en/docs/lang/nominal-structural/ .
В typescript преимущественно используется структурная типизация, однако есть несколько специальных случаев, когда типы начинают вести себя как номинальные. Это выглядит скорее как изъян в дизайне, чем осмысленное решение. История давняя, если интересно, следите за https://github.com/Microsoft/Typescript/issues/202 .
Мне кажется вы часто бросаетесь в какие-то крайности и противопоставления. Возможно это такой метод познания мира или авторский стиль. Но когда тема не является полярной, а содержит непрерывный спектр взаимосвязанных элементов, ваш подход скорее всего даёт неправильную картинку.
Система типов это не священная корова, а чисто утилитарный инструмент для проверки программы на наличие логических ошибок. Как существует великое множество различных логик со своими законами, так и дизайн каждой из таких систем отражает видение авторов конкретного языка программирования и является следствием каких-то решений и компромиссов. Закономеро, что в разных языках они отличаются. В пределе, выразительные возможности упираются в теорему Райса https://en.m.wikipedia.org/wiki/Rice%27s_theorem. Поэтому не существует какой-то единственно правильной или однозначно лучшей системы. Расслабьтесь и пользуйтесь тем, что даёт язык, либо попробуйте придумать свой.
habr.com/ru/post/477448
в
<code=typescript>function test(a: NoExtraProperties<A>) {} (тип NoExtraProperties можно нагуглить или самому написать)
Причем теоретически, я могу заставить тайпскрипт валидировать полное соответствие — как тогда мне называть его модель типизации? Понятия не имею.
«Ритуальная шелуха» в современных статически типизированных языках существет только в интерфейсах методов и объектов. И в динамических языках она также существует, только занимает гораздо больше места (в виде проверок на правильность типов аргументов и/или тестов вида «что если передать объект неправильного типа»). В «проектах» на 20 строк кода такого, конечно, может и не быть.
Вот недавно была задачка. В проекте использовались 2d координаты (x, y) в одном домене. Потом понадобилось соединить их с другим доменом в котором использовались 3d координаты (x, y, z) причем «y» там означала высота, и правильная конвертация была такая: (x, y) => (x, 0, y) т.е. y переименовывался в z
Я могу только представить какая это была бы боль в структурно типизированном языке или вообще языке без типов. Но к счастью язык был статически типизируемым и все что понадобилось — это пара предоставляемых IDE рефакторов плюс обновить несколько математических функций.
метры вместо килограмм — редкая. Секунды вместо миллисекунд — уже почаще.
Строчки/числа пришедшие из враждебного внешнего мира должны на этапе парсинга валидироваться/конвертироваться, а не добираться до внутренней логики (так то там вообще null или объект может оказаться, сервис то сторонний, и если это не чекать то получится null island), и здесь как раз языки со статической типизацией сделают все что могут — либо сконвертируют, либо выдадут ошибку сразу при парсинге. Так то еще есть локаль, и в некоторых странах десятичный разделитель это точка, в других запятая, в третьих что-то типа апострофа. Тут никакие операторы уже не помогут если сразу корректно не сконвертить.
Вон даже в NASA ньютоны с фунтами путали, и грохали межпланетный зонд из-за этого.
Так то если писать код сразу без ошибок, без архитектурных просчетов, без "новичков", сразу зная (и понимая) все ТЗ целиком, если писать код 1 раз и никогда его не читать — типы (а также комментарии и тесты, читаемые названия переменных, да и вообще функции) не нужны.
Ну и самое главное преимущества из-за которого я никогда не смогу продуктивно работать на динамическом языке — это то что с нормальным инструментарием можно всегда точно сказать что какой-то объект/функция не используется, и точно найти все использования. Это очень сильно играет на чистоту кода — любое "старье" можно быстро отрефакторить или выпилить с минимальными шансами что-то сломать.
Что такое "вполне типизированный"? Как F# в котором есть поддержка единиц измерения? Или как C?
f(t, v0, a) = v0 * t + a * t^2 / 2
f(1u"s", 1u"m/s", 1u"m/s^2") # выдаёт 1.5 m
f(1u"s", 1u"m/s", 1u"m/s") # выдаёт ошибку
f(1u"s", 1u"kg/s", 1u"kg/s^2") # 1.5 kg
если последний пример не нравится (хотя он имеет физический смысл), можно ограничить чтобы обязательно получалось расстояние:
f(t, v0, a) = v0 * t + a * t^2 / 2 |> u"m"
Тогда с килограммами пример тоже ошибку даст.
Регулярно так пишу, и весьма помогает что здесь разные единицы измерений имеют разный тип. При записи в поле какой-нибудь структуры можно быть уверенным, что там будут именно нужные единицы; причём совместимые типа сантиметров и парсек автоматом переведутся между собой как нужно.
Если функция s
дана в виде
s : (v : Speed) -> (a : Acc) -> (t : Secs) -> Meters
где
Speed : Type
Speed = Meters // Secs
Acc : Type
Acc = Speed // Secs
то я набросаю типы вроде
sStartsAt0 : s v a 0 = 0
sTimeLinear : (t1, t2 : Secs) -> s v 0 t1 + s v 0 t2 = s v 0 (t1 + t2)
sSpeedLinear : (v1, v2 : Speed) -> s v1 0 t + s v2 0 t = s (v1 + v2) 0 t
и так далее из физических соображений. Можно даже выразить, что производная s
по времени должна быть линейна по ускорению, например, и связать со скоростью и её приращением.
Да. Поэтому и мой комментарий:
Мне так наоборот хочется разделить все числа посильнее, чтобы метры с килограммами сложить было нельзя.
Упадет. Но по крайней мере в базе данных не окажется лицензионного соглашения в поле "широта" :)
Вообще-то все современные языки умеют приводить типы. И решать задачу "как получить int из json если там вдруг строка" должен автор библиотеки json сериализации (т.е. в 99% случаев — не пользователь библиотеки). Т.е. ситуация оказывается строго лучше — мы все также можем распарсить число пришедшее в json в виде строки, но только у нас теперь есть еще и дополнительные гарантии от компилятора.
Что-то не припомню, чтобы апологеты типов заявляли о ненужности тестов (а вот наоборот – бывало).
Он написал о ненужности юнит тестов, а не тестов вообще. Юнит тесты многие не любят
"многие не любят" — это что-то странное
Зато правда. У многих плохо тестируемый код, да и навыки написания тестов вообще и моков в частности не очень. В результате юнит-тесты в лучшем случае становятся очень хрупкими из-за большого количества моков, или вырождаются в интеграционные, а то и функциональные.
Задавать окружение какого-то класса путём поднятия всей системы — не юнит-тест
Как по-мне, то и надо тестировать юнит, который мы тестируем, а не те, в которые он ходит. Моки как раз и позволяют протестировать как работает тестируемый юнит, правильно ли он ходит в соседний, правильно ли обрабатывает то, что он возвращает. А не тестировать как два юнита работают в связки.
веб-серверы — это про nginx или про приложение на каком-нибудь фреймворке?
Нормально я их тестирую юнит тестами только для одного юнита с помощью моков. И общение с операционной системой типа файлов или часов мокаю. На JS/TS или PHP
общение с операционной системой уже не замокать.
О, а я читал в конце апреля статью, в которой рассказывалось про то, как мокали общение с OS
Странная у вас оценка. Инфраструктура для моков создаётся один раз и переиспользуется многократно.
Интересно, как по-вашему живут все те компании, которые предпочитают типы тестам и при этом пишут что-то сложнее сортировок.
но 0xd34df00d предпочёл продолжать оставаться на уровне программирования примитивов. а это говорит "мне неинтересно"
На самом деле на уровне программирования вещей для программирования примитивов, а там есть, где развернуться.
Какой-нибудь оптимизатор внутри компилятора вы предпочтёте тестировать или таки согласитесь на типы?
Вы мне рассказывали что якобы работаете в такой, но когда я спросил что делаете конкретно: спрятались за NDA
дескать работодатель из америки тут на хабре увидит и уволит
Я просто уважаю понятие свободных договоров, которые заключаю, и стараюсь их соблюдать хоть здесь на хабре, хоть с приятелем за банкой чая.
Впрочем, что, например, последнее место было хедж-фондом с торгами на бирже, я не особо скрываю.
берём юнит, который содержит код, ходящий в http или в Database.
его тест — юниттест или не юниттест?
Юнит тест это размытое понятие. Что конкретно имел ввиду 0xd34df00d под ним — надо спросить у него.
"многие не любят" — это что-то странное.
Почему. Многие не любят острую пищу. Чем это странное понятие? В реальности такое считается.
Напоминаю, мы не обсуждаем нужны ли юнит тесты, мы обсуждаем считает ли 0xd34df00d что никаие автоматические тесты не нужны. По его посту очевидно, что он считает что какую-то часть тестов он считает избыточной при статической типизации а не то, что никакие тесты не нужны.
Юнит тест это размытое понятие. Что конкретно имел ввиду 0xd34df00d под ним — надо спросить у него.Тест, не общающийся с «внешним миром». Неважно — замоканы другие компоненты или просто задача не требует общения.
Не важно где они там на практике встречаются. Важно какие тесты можно назвать юнит-тестами, а какие нельзя. Потому что если называть одним и тем же словом всё подряд — вас перестанут понимать.
Для этого — что-то там «понимать» или там «определять» — не только не нужно, но даже вредно. Ибо мешает набрасывать лапшу на уши.
Что как бы исключает зависимость от внешнего мира, не так ли? Зачем тест модуля, например, оплаты в интернет-магазине, который зависит от того, что вернёт, если вернёт, настоящий платежный шлюз? я пишк тесты модуля оплаты, а не тесты платежного шлюза.
Юнит тест — да, такому коду не написать.
В юнит-тесте я хочу тестировать свой модуль, а не внешнюю систему или соседний модуль. Если тест падает, то ошибку буду искать в своём модуле.
Так как раз, чтобы тестировать поведение кода внутри юнита, нужно избавиться от внешнего мира, иначе мы будем тестировать и код внутри модуля, и внешний мир одновременно. И упавший тест нам даже не подскажет где ошибка — внутри модуля или во внешнем мире.
Не могу понять откуда столько споров. Все зависит от того что конкретно вы хотите тестировать. Если поведение своего модуля то вы обязаны замокать окружение, что бы гарантировать единообразие и стабильность окружения.
Если вы хотите тестировать взаимодействие то, конечно, мокать нчего не надо. Но обычно это называют интеграционным тестированием, потому что чужой код / зависимость это уже другой модуль/юнит.
UPD:
Ещё раз: юниттест — это тест, тестирующий поведение кода внутри юнита.
Правильно, но чужая система не ваш юнит.
Да вам же и нужно:
тесты называются юниттестами если они тестят код внутри юнита.
Если тесты тестят внешний мир, то какие-же они юнит-тесты?
А избавляться от внешнего мира нужно по двум основным причинам:
- тесты работают быстрее
- упавший юнит-тест однозначно определяет, что проблема или в тесте, или в юните, то есть в коде, который мы сами написали
А за интеграцию юнитов друг с другом или юнита с внешним миром, отвечают, как ни странно, интеграционные тесты.
Смотря какой тест.
У меня есть, например, парсилка хабра. Там есть тест, что если натравить парсер на пост с таким-то ID, то получится статья с таким-то заголовком, таким-то текстом, такими-то тегами и такими-то комментами. Каждую из пары десятков индивидуальных функций в модуле парсера я не тестирую и тестировать не буду, важно лишь то, как они работают в совокупности.
Юнит-тест ли это или нет?
все вышеприведённые примеры (спутник упал, самолёт перевернуло, самолёт чуть ни упал, итп) приведены в аргумент «хорошо писать на языках с типами», но все эти примеры написаны именно на языках с типамиНу если вы знаете примеры самолётов с софтом на нетипизированных языках и можете аргументированно показать что им разницы операторов между целыми числами и строками хватило — то, пожалуйста, можете статью написать.
Но ничего подобного я от вас пока что не видел, сплошные рассуждения про великий и ужасный документооборот.
Тут же, знаете ли, всё просто, как с технарями и гуманитариями: если людей с техническим образованием, которые потом стали известны как гуманитарии — полно, а ноборот — как-то не очень, то сразу ясно чего стоит «гуманитарная одарённость».
А если документаоборот на статически типизированных языках реализуют (я сам, лично, этим занимался, когда ещё студентом был), а вот системы управления самолётами автомобилями — как-то нет, то… это, собственно, и всё, что нужно знать о динамической типизации.
А если документаоборот на статически типизированных языках реализуют (я сам, лично, этим занимался, когда ещё студентом был), а вот системы управления самолётами автомобилями — как-то нет, то… это, собственно, и всё, что нужно знать о динамической типизации.
Давайте все программы вот так писать:
>The fly-by-wire flight software for the Saab Gripen (a lightweight
>fighter) went a step further. It disallowed both subroutine calls and
>backward branches, except for the one at the bottom of the main loop.
>Control flow went forward only. Sometimes one piece of code had to leave
>a note for a later piece telling it what to do, but this worked out well
>for testing: all data was allocated statically, and monitoring those
>variables gave a clear picture of most everything the software was doing.
>The software did only the bare essentials, and of course, they were
>serious about thorough ground testing.
>
>No bug has ever been found in the «released for flight» versions of that
>code.
Давайте все программы вот так писать:Дорого так писать очень. Но вообще — можно и так.
Дорого так писать очень. Но вообще — можно и так.
Как бы я никогда не спорил с тем, что динамически типизированные языки не нужны. Они нужны.
Но не в тех случаях, когда вам нужна работающая программа без ошибок!
И да — я ни разу не макетолог и не продажник. Если мы порождаем, я извиняюсь, дерьмо — то я так и говорю: мы порождаем дерьмо. А не «конфету с альтернативным вкусом».
И дважды два для меня — четыре. Независимо от того, продаём мы или покупаем.
Ну вот так.
А если вам хочется изгнать математику из программирования, чтобы продавать «конфеты с альтернативным вкусом»… да ради бога: пока мне это не приходится расхлёбывать — продавайте кому угодно и что угодно… только без меня.
как раз динамически типизируемый язык позволяет делать меньше ошибок. Именно потому что код алгоритма очищен от ритуальных ненужностей и программист может видеть/понимать больше/лучшеТак пример самолёта или хотя бы автомобиля с управляющим ПО на таком языке — будет али нет?
пока ещё на Rust не написано ни одного полезного приложенияО как. Внезапно когда речь зашла за Rust — так это оказалось важно. А как про динамические языки говорили — так было достаточно веры в то, что «нет — но обязательно будет».
а TS популярен не столько из за типов сколько из за банально более развитого ООПВам самому-то не смешно? Хотя о чём это я: профессиональным демагогам сомнения неведомы. Верить в 10 противоречащих друг другу вещей одновременно — это бывает очень пользительно для зарплаты.
так я Вам вернул Ваш тезис, Вы не заметили?Заметил, конечно. Почему все демагоги считают, что они такие вумные, мне интересно?
Я уже про это писал явно: с демагогами (и с вами, в том числе) говорить о чём-либо бессмысленно.
О чём можно говорить с человеком, готовым одновременно верить во что угодно (а только лишь при этих условиях и можно «возращать кому-то ваш тезис») если ему это выгодно?
Единственная цель, которая может иметься в общении с демагогом — показать другим, вменяемым, людям, что перед нами — «демагог классический».
В идеале — тем, кто может такого персонажа уволить. И всё, собственно.
языки низкого уровня — традиционно с типами
Во всяких си и ассемблерах с типами очень бедно.
языки низкого уровня вследствие того что они низкого уровня, то позволяют писать более быстрый код
Разница в скорости условного Си и Си# в большинстве случаев при правильном написании кода невелика.
безопасность у языков низкого уровня хуже чем безопасность у языков высокого уровня: на программисте лежит управление памятью, например
Раст на вас смотрит с недоумением.
приход операционных систем общего назначения в такие отрасли как бортовые компы машин (и их автопилоты), самолётов — уже происходит и со временем других бортовых компов и не останется.
Системы реального времени никуда не деваются, и языки с GC туда не приглашают.
Было бы неплохо при этом знать, кто именно производит такие автомобили, чтобы держаться от них подальше (что в автосалоне, что на дороге).
Естественно не помогли, написано же, «никаких проверок типов».
код алгоритма очищен от ритуальных ненужностей
Простите!
const item = {
createdAt: new Date(),
}
// function isOutdated(item) {};
console.log(isOutdated(item));
Этот «чистейший алгоритм» отработает корректно?
Ответ — конечно нет. И динамически типизированный язык не позволил ни увидеть, ни понять ни больше, ни лучше. Докопаться до истины мы можем только изучив, что делает функция isOutdated.
function isOutdated(item) {
// createdAt is timestamp
return Date.now() - item.createdAt >= 123456;
};
Для примера я упростил все по максимуму, в реальности эти два куска кода могут быть даже в разных репозиториях.
Теперь посмотрим, как этот код выглядел бы на Typescript
const item = {
createdAt: new Date(),
}
// function isOutdated(item: { createdAt: Timestamp }): boolean {};
console.log(isOutdated(item)); // won't compile
Вам даже не нужно заглядывать внутрь функции, чтобы понять в чем ошибка.
function isOutdated(item: { createdAt: Timestamp }) {
return Date.now() - unwrap(item.createdAt) >= 123456;
};
Вывод такой, что строгая типизация позволяет добиться таки желаемого эффекта:
программист может видеть/понимать больше/лучше
Даже на динамически типизированном языке вы оперируете типами, просто они нигде не записаны, кроме как в голове у условного разработчика. Программист сменил место работы — с ним ушли важнейшие знания о проекте. Адекватный подход с точки зрения работодателя — это когда все знания о проекте остаются в нем.
если объект Date определит привод себя к чиселке — то может быть сравниваем с чиселками
а если объект Date определит привод к себе строчечки, то может быть сравниваем и со строчечками формата YYYY-MM-DD ...
А если и то, и то? Кстати, насколько мне известно, Date в JavaScript работает не так.
если объект Date определит привод себя к чиселке — то может быть сравниваем с чиселками
Есть разные способы привести момент времени к числу — из широко используемых, например, unix time и julian date.
а если объект Date определит привод к себе строчечки, то может быть сравниваем и со строчечками формата YYYY-MM-DD ...
Действительно, а американцев тут будем игнорировать.
int < 2500 = это просто год
int > 2500 = это timestamp
К счастью, никакой нормальный язык программированию, насколько я знаю, такой критерий для сравения дат и чисел не использует. Правда, очень смешное предложение.
date в int через unix time не позволит различить 2016-12-31T23:59:60 от 2017-01-01Т00:00:00 а между ними одна секунда
03-05-2020, например.
я о том что аргумент про типы «вот видите самолёт упал, а типы ему бы помогли» некорректны, поскольку самолёт падал именно на софте с типами.Покажите мне, пожалуйста, кретина, который утвердает что любая система типов позволяет избежать любых ошибок.
Для каждой конкретной ошибки — да, можно изобрести систему типов, которая позволит её избежать. Для всех сразу — нет, невозможно. Как минимум потому что невозможно избежать ошибок в постановке задачи.
То обстоятельство что я не привёл контраргумент не оставляет возможности наставивать на том, некорректность которого выяснилиГениально! Вы сами-то прочитайте, что вы написали, а? Если вы не привели контрагумент — то о чём мы тут вообще говорим?
Утверждение, извините, не заключалось в том, что любые типы могут помочь вам избежать любых ошибок.
Утверждение заключалось что чем сильнее система типов, тем меньше ошибок дойдут до стадии полёта. Так же утверждалась, что ваша идиотская идея «достаточно не складывать строки оператором плюс — и настенет нирванна» — не работает.
Оба утвеждения вполне себе хорошо иллюстрируются тем фактом, что ни один самолёт, софт для которого был бы написан на указанных вами принципах не смог не то, что упасть — он даже взлететь не смог!
Боинг как-то новости писал о применении Linux в бортовых компах и Эирбас тоже. Можно поискать.Поищите, поищите. Там Linux применяется только в системах, на которые не завязана безопасность. Ни в каком MCAS этого нету.
или вот скажем машины (более частый кейз) с автопилотами (которые пока называют помощниками вождению) это про безопасность или нет?Пока что это про «премию Дарвина» в основном.
какая операционка в Tesla? Linux же.Ну это уже шаг вперёд для Маска. Когда-то его из PayPal выперли потому, что он вообще Windows хотел вкрутить.
Но это всё — как раз нормально и естественно для «продавца воздуха».
Python де-факто это язык с динамической типизацией.Python там используется для тренировки моделей.
И даже там делаются попытки от этого уйти. Ибо ненадёжно это всё.
Но пока эти машинки по дорогам сами не ездят и сертификатов не получают — можно и Python, конечно.
Хинты для линтера приделали сравнительно недавно, но никаких ошибок компиляции эти хинты не вызывают.Пока не вызвают. А дальше — либо начнут вызывать, либо Python будет на что-то другое заменён.
So, если в настоящее время нет самолётов под управлением скриптового языка с динамической типизацией, то в будущем обязательно будут :) Всё к этому идёт.Поживём — увидим.
О чём-то можно будет говорить после банкротства той самой Теслы. Ибо пока существует достаточно людей, готовых платить на неработающий автопилот как за работающий… ни качественные программы, ни, соотвественно, типы и ФП — действительно никому не нужны.
У вас логическая ошибка: якобы переход к типизированным языкам обязательно вызван желанием повысить быстродействие и обязательно сопровождается снижением надёжности. А это не так, хотя бы потому, что есть, например, Java, вполне себе статически типизированный язык, но с исключениями и без SIGBUS.
Python там используется для тренировки моделей.
И даже там делаются попытки от этого уйти. Ибо ненадёжно это всё.
Общался не так давно с людьми, которые исследуют возможность формальной верификации нейросеток (тема не такая публишабельная, конечно, как ещё стопицот слоёв бросить на стену и смотреть, что прилипнет, но тем не менее). Так что уйти хотят не только там и не только переходом на свифт.
Какое отношение управление памятью и компиляция имеет к типизации?
Есть нетипизированные языки с компиляторами, есть возможность запускать типизированные программы без компиляции.
Ладно, закроем глаза на очевидное противоречие этого коммента вашему предыдущему.
Так причём статические типы к подсчёту каждого байта? Каждый байт удобнее считать с ассемблером, который вот ну совсем нетипизированный.
Вы считаете типизацией явное указание, сколько байт из памяти надо загрузить в какой регистр?
Практика показывает, что, увы, языки с наиболее продвинутыми системами типов помедленнее ассемблера (или даже грамотно написанного С в значимой части случаев).
Но у вас в очередной раз некорректное обращение импликации — даже если отдельные виды типизации когда-то в отдельных случаях решали эту проблему, это не значит, что типизация сегодня нужна для того, чтобы сказать, кто сколько байт занимает.
выбор X, Y обычно делают вообще не люди, а обстоятельства: на рынке труда много дешёвых спецов по языку X
То есть люди сделали выбор и выучили язык Х.
первый спец занявшийся этим проектом знал только X
то же самое — программист сам выбрал язык.
Ну и самое главное преимущества из-за которого я никогда не смогу продуктивно работать на динамическом языке — это то что с нормальным инструментарием можно всегда точно сказать что какой-то объект/функция не используется, и точно найти все использования.
А что мешает это делать при динамической типизации? Ну кроме штук вроде variable functions в пхп.
Тот факт, что для того чтобы сделать такой вывод, надо расставить, пусть даже в уме, кучу статических "маркеров" по затронутой части программы. А если это возможно сделать — то чем нам статическая типизация помешала?
Его написать не просто сложно, его зачастую написать невозможно.
Кстати, логика создателей линтеров порой и правда "потрясающая".
Если линтер написать невозможно => компилятор тоже написать невозможно.
Ну разумеется. А что, существуют полноценные AOT-компиляторы для языков с динамической типизацией? Насколько я знаю, в лучшем случае есть JIT, а в худшем — так простым интерпретатором всё и ограничивается.
самый синтаксически богатый язык — Perl. Он же самый динамический. для него написан прекраснейший PerlCritic и серия линтеров.
И что, там можно во всех случаях точно установить обращается ли достаточно сложная программа к конкретному методу или полю объекта? Без доказательств я в такое не поверю.
Типы позволяют установить это во всех случаях, с некоторой помощью программиста.
Линтеры не позволяют этого установить во всех случаях, даже с помощью программиста.
а типы разве во всех случаях это устанавливают?
False negative'ов о том, что какая-то функция не используется, или что функци из импортированного модуля не используются, у меня в хаскеле не было. Правда, в первом случае нет кроссмодульного анализа, но это скорее вопрос к тулингу — нет причин запустить одну и ту же процедуру на всех модулях и сделать выводы.
ну а совершённую логическую ошибку никакой тип не находит
Тип
sumZeroLeftId : (b : Int) -> sum 0 b = b
находит.
Впрочем, когда мы это обсуждали в прошлый раз, то в ответ на это выяснилось, что на самом деле такой код никто не пишет, поэтому это неинтересно обсуждать, поэтому неважно, что типы тут что-то находят. Непонятно, зачем тогда приводить этот пример, правда, ну да ладно.
Ну вот пример выше с координатами
Дано — куча функций, некоторые работают с 2d координатами, некоторые с 3d, некоторые даже и с теми и с теми.
Задача — у всех использований 2d координат заменить y на z (т.е. превратить 2d координату в 3d). 3d координаты не трогать.
Как решить эту задачу точно? В смысле без ложноположительных и ложноотрицательных срабатваний?
Бонус: Часть данных грузится с диска (к примеру)
Мега бонус: Если нужно поменять не все 2d координаты, а только часть (но существенную часть). В системе с типами можно просто "начать" менять, а дальше просто пофиксить все места где компилятор говорит что "вот тут тип неправильный".
Еще на всякий случай скажу что это не искусственная задача.
Как решить эту задачу точно? В смысле без ложноположительных и ложноотрицательных срабатваний?А зачем её решать?
Можно же, вместо этого, попариться с заказчиком в баньке, перетереть всё — и заработать больше денег, чем это сделаете вы, когда что-то там докажите.
Вся логика rsync, по большому счёту, к этому сводится (хотя он прямо в этих терминах вам этого не скажет).
Кроме того, я не согласен что не ставя типы экономится время.
Немножно времени экономися сначала, но потом:
Чуть больше времени тратится когда в следующий раз этот код пришлось почитать
Чуть больше времени тратится на написание дополнительных проверок который в системе с типами делать не нужно
Чуть больше времени тратится на написание дополнительных тестов, которые в системе с типами писать не нужно
Чуть больше времени тратится на коментирование (в тех случаях когда имя типа и есть достаточный комментарий)
Чуть больше времени тратится потом на рефакторинг
Систему с большим числом использований потом сложно менять "мало ли кто полагается на текущую деталь реализации". А для работы со старой системой приходится тратить чуть больше времени
и т.п.
В итоге, код которые "write once" и больше никогда не нужно менять/читать (скрипты и простейшие сервисы) получается выгоднее писать на языках без типизации. Но как только дело разрастается до серьезных вещей — на все приходится тратить "чуть больше времени".
Распарсиваете сорс, смотрите, что вызывает что, строите дерево, делаете выводы.
Ну что-нибудь банальное типа такого:
foo = [False] * 3
foo[0] = 42
foo[1] = 57
foo[2] = 115
Foo(foo)
Понять — нужно ли у вас в функции Foo
обрабатывать случай, когда в массиве есть False
— невозможно, если вы не можете понять — отработают у вас все присваивания или иногда могут не отработать.А это уже, может привести, к вызову функций типа
MakeDefaultFoo0
и прочее.Если же вы подобные вещи никогда не пишите и все типы у вас всегда однозначно выводятся — то вам динамическая типизация «нафиг не упала» и вы вполне могли бы работать и на языке со статической без указания типов (Haskell, OCaml, возможно даже Google Go).
Еще можно добавить что при написании библиотек приходится выставлять публичный интерфейс наружу (или, к примеру, читать json по сети), и вот тут то без указаний типа никакой линтер не скажет ничего более ценного чем "ээээ ну а тут будет any"
Вернее работает только когда вы используете язык с динамической типизацией как язык со статической типизацией, но без явного указания типа.Я пишу почти одинаково что на статике, что на динамике. Мне не особо нужен тайпчекинг, потому что если я что-то сделаю не так, всё развалится. Зато мне очень нужна возможность запускать то, что я пишу каждые 1-5 минут, т.е. либо интерпретируемый язык (а именно они как правило динамически типизированные), либо очень бысто компилирующийся, типа JAI, который сейчас в бете.
Зато мне очень нужна возможность запускать то, что я пишу каждые 1-5 минутЭто… Если достаточно долго месить чан с перловой кашей, в синтаксическом мусоре можно рано или поздно узреть лик Ларри Уолла?
Да, есть такая технология… какое она имеет отношение к программированию?
Или, если это не попытка узреть лик Ларри Уолла — то что вы за задачу решаете, что требования к ней меняются каждые 1-5 минут? А если они не меняются — то зачем вам код-то запускать, извините?
Напишите — запустите, делов-то. У Haskell, кстати, есть неплохой REPL для экспериментов.
Или, если это не попытка узреть лик Ларри Уолла — то что вы за задачу решаете, что требования к ней меняются каждые 1-5 минут?Процедурная графика. Там вообще строгих требований быть не может. Если у меня там цепочка аффинных преобразований и подобного, накосячить (например, с их порядком) так, что система типов не отловит — раз плюнуть.
И этот метод не про требования — он про то, что если постоянно запускать то, что ты пишешь, гораздо меньше времени уходит на дебаг, потому что почти все ошибки исправляются сразу. Короткий feedback loop.
И с Ларри Уоллом — это не ко мне, я не о нём практически ничего кроме имени не знаю.
Если у меня там цепочка аффинных преобразований и подобного, накосячить (например, с их порядком) так, что система типов не отловит — раз плюнуть.Ну и зачем для этого запускать каждые 5 минут систему на 100 миллионов строк?
Вполне хватит «песочницы», которая не будет требовать сборки кода по часу.
И с Ларри Уоллом — это не ко мне, я не о нём практически ничего кроме имени не знаю.Вы бы весь рассказ прочитали. Там «лик Ларри Уолла» — не более чем метафора…
сильно сомневаюсь что например Google имеет хоть один проект на 100 млн строк кода.5 лет назад считали
У Гугла вообще нет деления на «проекты». Библиотеки — есть. Модули — есть. Проектов — нет.
Ну или если считать что вот Android и Chrome (которые живут в отдельном проекте) и «неявно» код не шарят — то у Гугла три проекта: Android, Chrome, и, собственно, то, что называется Google3.
Где как раз и живут все эти 2 миллиарда строк и откуда собираются почти все продукты Гугла.
сильно сомневаюсь что например Google имеет хоть один проект на 100 млн строк кода.
Откуда у них такие объемы? Такое только в яндекса наверное.
Интерпретируемые языки дают это делать, причём удобно, потому что они именно под это заточены.TypeScript — вполне себе интерпретируемый, но под это не заточен. Или Dart.
А вот Go — компилируемый, но там как раз внимательно следили за тем, чтобы компиляция происходила достаточно быстро для того, чтобы
go run
имел смысл.Есть deno, которая работает как интерпретатор (точнее, внутри-то он все равно наверняка компилирует, но снаружи этого не очень видно).
А вот "не заточен под интерпретацию" никакая deno не изменит, потому что в тайпскрипте в принципе невозможно тайпчекнуть пару строчек не загружая все модули.
Зато мне очень нужна возможность запускать то, что я пишу каждые 1-5 минут, т.е. либо интерпретируемый язык (а именно они как правило динамически типизированные), либо очень бысто компилирующийся, типа JAI, который сейчас в бете.
У меня прямо сейчас открыт хаскелевский репл, где я после каких-то модификаций кода пишу :r
и дальше играюсь в репле. Загрузить все 20 модулей моего проекта (которые компиляются в релизе секунд 20) занимает пару секунд, перезагрузка только изменившихся по :r
— чаще меньше секунды.
Впрочем, сейчас я куда меньше полагаюсь на репл, чем несколько лет назад, потому что сейчас про большинство ошибок мне сразу говорит IDE из-за интеграции с тайпчекером.
Про ошибки, о которых не говорит тайпчекер вроде уже обсуждали.
Ну а я «играюсь» прямо в коде.
Ну так тем более, написали что-то в коде, переключились на репл, вызвали только что написанную функцию.
Чем пользуетесь для хаскеля, не подскажете?
Idea + IntelliJ-Haskell (по наводке PsyHaSTe — раньше пользовался vim + coc + hie, vim, конечно, мощнее как редактор, но hie жрёт память как не в себя).
Спасибо!
Примитивный алгоритм сломается на первом же колбеке.
Чуть более сложный алгоритм сломается на первой же обобщенной функции, вроде метода map
у массива или функций из библиотеки lodash.
А дальше вы "напарываетесь" на невозможность вывода типа и вынуждены просить программиста помочь вам аннотациями.
Но даже если вам повезёт и вы ни на что не напоритесь — ваш линтер внезапно становиться ещё одним инструментом статической типизации, притом самым лучшим :-) Динамическая типизация языка исчезла в тот момент, когда линтер разметил все функции в программе.
def call_by_name(name, *args, **kwargs):
return eval(str(name))(*args, **kwargs)
Удачи
это то что с нормальным инструментарием можно всегда точно сказать что какой-то объект/функция не используется, и точно найти все использования
Как статическая типизация поможет в таком случае?
Особенно если name вы получаете откуда-то извне?
Как статическая типизация поможет в таком случае?
Запретит использовать eval.
А для чего вы используете eval в своих проектах?
А так вы рассказываете что парсите одним проходом, а последующий евал будет работать быстро, так в комплируемых языках одного прохода бы уже возможно хватило :) ну типа комон, кодогенерация для парсинга для скорости?
стоп что?
для парсинга для скорости евал?
что что? :-D
для скорости евал?
Да без проблем: https://habr.com/ru/post/158403/
Но это не достоинство евал или яваскрипта, это его убогость и горбатость.
При этом в компилируемых языках это все точно так же можно сделать создав «жирную» лямбду из многих маленьких.
Это не только для яваскрипта работает. Схожие трюки можно даже на плюсах или Rust проворачивать, только понадобится с LLVM слинковаться за неимением eval :-)
И это у меня вызывает приступ дикого смеха, что в JS такие наивные костыли работают быстрее исходного кода :)
Тут опять же стоит задаться вопросом а что мы собственно выжимаем, если код такой на каждый чох меняется то может подход сменить целиком?
А если не часто то кодогенерацию можно и на этап компиляции перенести.
От задачи зависит, при подходящих задачах вроде численного интегрирования введенной пользователем функции можно и на плюсах +500% получить.
В нормальных языках существуют нормальные же способы решать подобные задачи.
Конкретно в случае парсеров хорошо работает кодогенерация. Она, к слову, и для скриптовых языков хорошо работает, даже быстрее чем eval.
Разумеется, генерировать надо заранее.
Например в протоколе части-блоки определяют следующие блоки (например передаётся длина и далее собственно тело) итп.
А в чём, собственно, проблема считать сначала длину, а потом тело? И зачем тут вообще eval?
в случае с наличием eval, мы просто однопроходно преобразуем это в код и вызываем на нём eval. Получаем очень быстрый шаблонизатор.
А без eval мы просто однопроходно преобразуем это в код и подключаем как модуль. Получаем такой же быстрый шаблонизатор, но с ускоренным стартом и возможностью проверить типы.
или при помощи eval сделегировали эту работу более быстрому by design интерпретатору языка
Конкретно в случае "длины и тела" нормально считать быстрее. eval вам сожрёт всю производительность на постоянных операциях парсинга переданного в него кода.
подключаем как модуль — это и есть вызов eval, просто названо чуток по другому.
Нет. Даже если оно реализовано через eval — это всего лишь одна из реализаций, которая не мешает статическому тулингу.
Оно мешает, когда eval находится в вашем коде.
Код загрузчика модулей и прочую инфраструктуру "показывать" тайпчекеру не обязательно — тот может подгрузить модуль самостоятельно, без использования eval.
Вы разбираете поток данных используя управляющие структуры на «медленном языке», или при помощи eval сделегировали эту работу более быстрому by design интерпретатору языкаНу то есть вы сначала накакали себе в карман, а потом пытаетесь придумать как из этой кучи дерьма выудить бумажник?
А может того… не какать? Хотя бы себе в карман?
что такое eval? это вызов интерпретатора/компилятора во время исполнения программы.Технически — да. А идеологически — нет.
Либо вы вызываете
eval
на код, который написал ваш разработчик (тогда eval
— эта трата дикого количества энергии впустую), либо на код, который к вам пришёл извне (тогда это — почти всегда дыра в безопасности).то что решили использовать язык высокого уровня?То, что выбрали язык, который не позволяет написать на нём достаточно быстрый парсер.
Haskell, скажем, язык достаточно высокого уровня (хотя я его и не люблю), парсеры там пишутся без всяких
eval
и скорость их достаточна для того, чтобы о ней, в 99% случаев не думать (а в том 1% случаев где думать надо — никакой eval
PHP или Python всё равно не спасёт).скорость разработки обычно существенно важнее скорости работы.В парадигме «нам не нужны работающие программы, нам нужно убедить заказчика в том, что порождённая нами куча дерьма — это конфетка»… согласен.
Либо вы вызываете eval на код, который написал ваш разработчик (тогда eval — эта трата дикого количества энергии впустую), либо на код, который к вам пришёл извне (тогда это — почти всегда дыра в безопасности).
На самом деле, есть третий вариант — вызов eval на код, который сгенерировал разработчик. Иногда это даже имеет смысл, на тех языках где динамической компиляции из AST не завезли.
На самом деле, есть третий вариант — вызов eval на код, который сгенерировал разработчик.Чем этот третий вариант отличается от первого?
Иногда это даже имеет смысл, на тех языках где динамической компиляции из AST не завезли.Компилятор напустить на то, что вы там породили… религия не позволяет?
Ну честное слово, что за детский сад: lex и yacc — это ни разу не новинка.
Чем этот третий вариант отличается от первого?
Тем, что код появился только в рантайме. Вы ещё спросите зачем Java и .NET используют JIT, когда AOT компиляция такая замечательная!
Ну честное слово, что за детский сад: lex и yacc — это ни разу не новинка.
А кто тут предлагает от них отказаться (кроме вас)?
Вы ещё спросите зачем Java и .NET используют JIT, когда AOT компиляция такая замечательная!Спрошу, разумеется. Вот на Android/iOS JIT тупо взяли — и запретили. И ничего: и C# и Java, оказываются могут в AOT, если нужно.
Тем, что код появился только в рантайме.Откуда он взялся и почему его нельзя было скопилировать нормальным компилятором?
Откуда он взялся и почему его нельзя было скопилировать нормальным компилятором?
- зависимость от окружения
- обобщенный код, который тупо неудобно компилировать заранее (вроде того как в С++ есть возможность выносить реализации шаблонных функций в отдельные единицы компиляции — но нахрена?)
- зависимость от ввода пользователя
Выбирайте любую из причин, можно сразу несколько.
Откуда он взялся и почему его нельзя было скопилировать нормальным компилятором?
Как конкретный пример с одной прошлой работы — я писал компилятор для одного предметно-специфичного языка. Соответственно, ему по сети приходит программа на этом языке с просьбой её сохранить, а потом сваливаются данные, которые через эту программу надо прогнать. Там было несколько разных бекендов, от тупого интепретатора (который отлично подходит для отладки) до генерации LLVM IR и этакого JIT'а.
Желать eval как-то не приходилось. Из общих соображений — едва ли оно было бы существенно быстрее тупого интерпретатора.
Но это очень частный случай, за всю практику такой проект был, наверное, один.
сделегировали эту работу более быстрому by design интерпретатору языка
У нормальных людей бай дизайн есть компилятор который позволяет не обогревать вселенную в холостую интерпретированном евалов.
Я щас вам может открою глаза, но на любом современном языке это можно воспроизвести например генерацией лямбды в рантайме, или дерева лямбд которые потом будучи запущенными быстренько-быстренько вам все посчитают и без интепретаций, и с проверками типов.
С такой аргументацией можно вовсе от интерпретируемых языков отказаться — но они почему-то существуют.
Но вот что вызывает удивление — так это непрерывный «бег в колесе»: «ой, мы написали дерьмовый код на дерьмовом языке и получили дерьмо… а как бы нам из него конфетку сделать, а?». Ну если вы хотели конфетку — то зачем вы в дерьмо-то полезли?
У меня есть куча скриптов на python и никогда не было идей даже думать над тем, с какой скоростью они работают. Ибо они дёргают другие программы и сами по себе не являются боттлнеком даже и близко.
Если же их скорость перестанет меня устраивать — то последнее, о чём я буду думать — так это о том, чтобы прикрутить туда
eval
.Переписать на компилируемый язык (неважно какой: Go, C++, Rust, Haskell… любой язык о скорости которого можно, в принципе, как-то рассуждать) — первое, что стоит сделать.
Применять «грязные хаки» — когда вы сами себя «загнали в угол» и хорошего выхода у вас нет. И этого, в общем, следует избегать…
Переписывать проект на другой язык из-за одного места в коде?
И, если речь идёт о браузере, добавить загружаться пару мегабайт рантайма выбранного языка для WASM?
А это точно лучше одного аккуратного eval?
Переписывать проект на другой язык из-за одного места в коде?Нет. Не из-за одного места в коде. Из-за того, что задача стала требовать скорости.
Это уже на основе опыта. Ну нельзя расшить одно место — и получить удовлетворительную скорость. «Разошьёте» одно место, получите затык в другом. И, рано или поздно, всё равно придётся переделывать.
Самый эпичный пример, который я знаю — это обсуждение опуса «Please don't use Python except for small scripts» внутри гугла. Полного текста у меня нет, но фишка там не в дискуссии как таковой, а в метадискуссии.
Потому что когда эта вещь была написана — сразу поднялась дискуссия на тему: «ну как же так — мы же все используем для Code review Mondrian, написанный на Python… его, правда, сам Гвидо лично написал, он, наверное, обладал тайным знанием… потому все проекты на Python разваливаются, а Mondrian — живёт и здравствует».
Дальше — это некоторое время обсуждалось… пытались понять — что Гвидо сделал «так» и что все остальные сделали «не так»…
А потом прорезался тонкий голосок какого-то новичка с примерно следующим текстом «я вообще в Python — небольшой специалист, да и в Гугле работаю без году неделя… но как участник прокта по переписывания Mondrian на Java могу сказать следующее: ну и дальше был какой-то текст».
Дискуссия, в общем, заглохла почти сразу. Потому что предмет для дискуссии, в общем-то, исчез.
P.S. На самом деле там было даже два форка: внутренний и внешний. И я даже не знаю над каким из них тот инжинер работал. Но это даже и неважно, так как Python извели из обоих.
А это точно лучше одного аккуратного eval?Ну вот не видел я чтобы «один аккуратный
eval
» менял картину.И, если речь идёт о браузере, добавить загружаться пару мегабайт рантайма выбранного языка для WASM?Браузер — это статья особая…
Как известно, бег в мешках и просто бег — это две сильно разные спортивные дисциплины. Очень сильно. Тот факт, что в брайузере есть особый, выделенный (и достаточно-таки убогий) язык, который в него встроен — очень сильно меняет картину мира. Тут я просто не могу ничего посоветовать… потому что я «в мешке» бегал очень мало.
Например в протоколе части-блоки определяют следующие блоки (например передаётся длина и далее собственно тело) итп.Ужжжаснейшая, сложжжнейшая задача! Главное — хорошо отваричиваться когда вам доку на ParSec или Ragel будут подсовывать.
в случае с наличием eval, мы просто однопроходно преобразуем это в код и вызываем на нём eval. Получаем очень быстрый шаблонизатор.Не. Получаем не шаблонизатор, а дыру в безопасности. И потом лет 10, пока этот шаблонизатор не «выпилят» — с неё кормимся.
в общем применений eval очень много, применений красивыхВ парадигме «главное — убедить заказчика, что баги — это нормально» да, никто не спорит.
Нет, это про кодогенерацию и метапрограммирование. Они с типами как раз отлично получаются. На Haskell есть и шаблоны и парсеры и куча всего ещё.программы, которые могут писать другие программы — самые счастливые программы в мире!это вот про такие алгоритмы
А
eval
— это про дыры в безопасности и «Job Security».Дыру в безопасности мы получаем, если в исполняемый код попадают входящие данные as is.
А типы как раз позволяют проверить, что данные попадают только должным образом проверенные и отфильтрованные.
Не только типы это позволяют. Да и сама проверка должна быть плюс-минус одинаковой.
entropy
Как вы будете тестировать криптографический код, кстати? Или это тоже слишком примитивные задачи для вас?
Eval — это дыра в безопасности исключительно в рантаймах без поддержки песочниц. Но даже для js песочницу сделать вполне реально.
Ну и Java тоже не шмогла.
В обоих случаях попытки затыкать бесконечные дыры были, в конце-концов, прекращены и эксперимент прикрыли.
«Почему-то» (подумайте почему) более-менее вменяемую «песочницу» удаётся сделать только в языках, где ни «песочница», ни «eval» не нужны…
Всё уже давно есть в $mol:
https://github.com/eigenmethod/mol/tree/master/func/sandbox
В онлайне повзламывать можете тут: https://calc.hyoo.ru/#title=Sandbox/A1=%3Dmax%282**3%2C3**2%29
Создать полезный, но небезопасный sandbox — тоже: просто ничего не проверяйте — будет возможность делать что угодно, но и безопасности не будет тоже.
Пока я не очень вижу — куда и для чего можно было бы применить ваш sandbox, потому, скорее, он ближе к первому варианту.
С этого все начинают. Судя по количеству упоминаний оной песочницы в Internet её пока ни для чего серьёзного использовать никто даже не пробовал.
По второй ссылке как раз пример практичного использования. Как взломаете — заходите.
А пока, вот вам ещё интересный пример с объявлением функции и применением её для свёртки диапазона: https://calc.hyoo.ru/#title=Sandbox/A1=reducer%20%3D%20%28a%2Cb%29%3D%3E%20%28a%2Bb%29%2F2/B1=1/A2=result%20%3D%20%28B1%3AC2%29.reduce%28_.reducer%29/B2=2/C1=3/C2=4
По второй ссылке как раз пример практичного использования.По второй ссылке я вижу пример самолюбования «смотрите как я могу».
Всё это действительно построено вокруг «безопасного режима» (в данном случае уже JavaScript, а не PHP и не Python), но поскольку на практике эту песочницу, насколько я знаю, особо не применяют, то попыток уйти от режима «смотрите как я могу» к чему-то, что можно реально использовать и, далее, к бесконечному затыванию дыр — пока и нет.
Разработчики PHP и Python ведь тоже не идиоты — они несколько лет пытались найти эту точку между бесполезной и дырявой песочницами.
Если не смогли сделать песочницу за пару лет — значит идиоты. Не идиоты — это разработчики wasm, nacl, web workers и тд, которые смогли.
Вы давайте не фантазируйте про дыры и бесполезность. Если кому-то когда-то на каком-то языке что-то не удалось — это ничего не говорит ни о реализуемости, ни даже о сложности реализации.
Когда нечего возразить — плюнь в спину карму и иди с миром.
как на скриптовом языке сделать быстрый парсер регулярной структуры, чтобы скорость парсера была соизмерима со скоростью компилируемого языка?
Под структурой понимаются внешние данные или структуры данных самого языка? Если первое, то никак. Если второе, то вы и не сделали парсер, а воспользовались уже готовым.
Ну и так, к слову, на одних скриптовых языках свет клином не сошёлся.
возможно есть компилируемые языки с eval, но я лично о них не знаю.Потому что не хотите знаить, очевидно. Вот, пожалуйста.
Да, его вызвать не так просто, как
eval
в скриптовых языках… но это, скорее, преимущество, а не недостаток.В .NET есть разные штуки чтобы генерировать промежуточный код в рантайме. Можно компилировать ExpressionTree или генерировать байткод.
Можно скомпилировать DLL из исходного текста и подгрузить в процесс.
В стандартной библиотеке регекспы компилятся.
ну вот берём скриптовый язык.
он by design медленнее компилируемого.
То есть, вы берёте какую-то проблему, специфичную для скриптового языка, рассматриваете инструмент, подходящий для её решения, а потом говорите, что если в других языках этого инструмента нет (при этом неважно, что соответствующей проблемы тоже нет), то они заведомо хуже. Я ничего не упустил?
расскажите о "нормальных языках" скриптовых и без eval
Зачем обязательно скриптовых?
Такие, как внедрение уязвимостей с иньекцией постороннего кода, ага.
Код типа object[methodName]()
, где name
— строка, полученная извне, вполне валидный для статически типизированного TypeScript. Но требует или тайпгварда, или каста. На практие очень часто только каст или тайпгвард не очень честный, например, проверяет, что строка только.
Ну кому как. Мне так наоборот хочется разделить все числа посильнее, чтобы метры с килограммами сложить было нельзя. Или чтобы id пользователя и id товара были разные типы, несмотря на то что оба номинально int (или string, неважно)
Поддержую. Сам иногда добавляю в свой проэкт DDD Value Object в виде структуры с неявным кастом, даже когда внутри будет только одно поле. Вот пример для Nullable от MS для понимания.
что в языке надо разделить строковые и числовые операторы.
Это до рождения PHP или после?
Технического обоснования нетСмотря что считать «техническим обоснованием».
На самом деле разница между Perl и Python сформилирована прямо в их девизах: TMTOWTDI против Должен существовать один — и, желательно, только один — очевидный способ сделать это.
Первый подход — идеален для порождения куч дерьма, которые обеспечивают потрясающую Job Security и возможность «вешать лапшу на уши» заказчикам, второй — позволяет добиться безопасности и надёжности (и, разумеется, несмотря на то, что является, формально, девизом Python — куда больше подходит таким языкам как Haskell или Idris).
В успехе Python виноват исключительно Google, «в котором все мечтают работать».То есть Google махнул палочкой — и Python вырос с 2-3% до 10%, махнул ещё раз — Perl упал с 10% до 1%.
А в успехе Rust тогда кто виноват? Только не нужно говорить, что «20е место — это не успех». Для языка, заточееного под реальную безопасность (а не о разговорах о ней) — 20е место это вполне себе успех.
Ибо в современном мире безопасность — не так, чтобы много кому нужна… а вот разговоры о ней — да, они продаются.
контексты, генераторы yield суффиксные if-else, циклы и генераторы, дцать типов-примитивов (set, tuple) итдТем не менее — там регулярно рассматриваются ситуации, когда нарушения этого принципа считаются минусом и обсужлается что-то типа такого.
так что этот принцип не более чем маркетинговая декларацияНу надо же. То есть в Perl — тоже регулярно обсуждается как убрать всякие «недостатки молодости», избавиться от TMTOWTDI и сделать всё правильно и красиво? Ссылочками не поделитесь?
Я то я, как бы, только про потуги создать Perl 6 знаю — и там натащили в язык столько всякого TMTOWTDI… что мало не покажется.
да этот принцип исповедуется коммюнити Perl, но это не свойство языка.Да, это свойство коммьюнити. Потому я и не могу сказать — отказ от Perl это техническое решение или нет.
этот принцип относится к любому языкуНи в коем случае. В большинстве других языков вопрос «как лучше сделать: через A или через B» считается нормальным. Да, на него не всегда удаётся дать однозначный ответ, иногда сама специфика задачи не позволяет сформулировать какое-то одно решение… но это считается нормальной и правильной целью.
В Perl же такие дискуссии, почти всегда, заканчиваются тем, что кто-то произносит TMTOWTDI… и объясняет что сам вопрос неверен: зачем вам, собственно, знать что лучше — используйте то, что вам левая пятка подсказывает в данном конкретном месте.
Есть некоторое количество людей, пытающихся как-то «уменьшить масштаб разрушений» от TMTOWTDI… но сама идея, что подход TMTOWTDI — плох и что TMTOWTDI является злом (хотя иногда и неизбежным, увы)… она в головах апологетов Perl никак не укладывается.
Увы, коммюнити питон далеко от этого принципа.Нет, конечно. В Python регулярно добавляют вещи, упрощающие программирования. И — при этом приходится отходить от принципа «должен быть только один способ сделать это».
Но когда это происходит — там никогда не забывают о том, что сделав что-то проще они, одновременно, делают и язык сложнее — в том числе для изучения новичками.
Потому упрощение должно быть достаточно существенным, чтобы его ввели в язык.
Взлёт популярности Python — это исключительно GoogleИнтересно только «как». Так-то у Google ни одного серьёзного проекта нет на Python, кроме TensorFlow, нет поддержки Python ни в Android ни в браузере… что вообще Google такого сделал, чтобы Python популярен стал? В TensorFlow его поиспользовал? Ну так это 2015й год, на графике популярности Python в 2015м ни скачков вверх, ни скачков вниз не наблюдается…
Вот Go — да, тут без поддержки Google явно ничего бы не вышло… но Python? Каким боком тут Google?
Объясните мне, как вы для себя разобрались в моделях типизаций — они же все размыты