[JavaScript, Программирование, Scala] Ко-вариантность и типы данных
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Тема вариантов в программировании вызывает кучу сложностей в понимании, по мне это проблема в том, что в качестве объяснения берут не всегда успешные метафоры - контейнеры.Я надеюсь что может у меня получиться объяснить эту тему с другой стороны используя метафоры “присвоения” в разрезе лямбд.Зачем вообще эта вариантность нужна ?В целом без вариантности можно жить и спокойно программировать, это не такая уж архиважная тема, у нас есть множество примеров языков программирования в которых это качество не отображено.Ко-вариантность это о типах данных и их контроле со стороны компиляторов. И ровно с этого места надо откатиться и сказать о типах данных и зачем это нам нужно.Flashback к типамТипы данных сами по себе тоже не являются сверхважной темой, есть языки в которых тип данных не особенно нужны, например ассемблер, brainfuck, РЕФАЛ.В том же РЕФАЛ или ассемблере очень легко перепутать к кому типу относиться переменная, и очень легко, например можно допустить что из одной строки я вычту другую строку, просто опечатка, никакого злого умысла.В языках с поддержкой типов, компилятор увидел бы это опечатку и не дал бы мне скомпилировать программу, но… например JS
> 'str-a' - 'str-b'
NaN
JS (JavaScript) Спокойно этот код проглатывает, мне скажут что это не баг, это фича, ок, допустим, тогда я возьму Python
>>> 'str-a' - 'str-b'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for -: 'str' and 'str'
Или Java
jshell> "str-a" - "str-b"
| Error:
| bad operand types for binary operator '-'
| first type: java.lang.String
| second type: java.lang.String
| "str-a" - "str-b"
| ^---------------^
То есть я клоню к тому, что считать багом или фичей - зависит от создателей языка.А мне как пользователю например вообще без разницы на каком языке написана та или иная программа, мне важно чтоб она работала.А как программисту, решающему задачу конкретного пользователя, я выберу тот язык, который будет удобен мне для решения задачи и я не хотел бы сильно заварчиваться на особенности языков, знать особенности работы с тем или иным типом данным.Еще пример: может быть такой мой сценарий, допустим вчера я написал на Groovy вот такой код
groovy> def fun1( a, b ){
groovy> return a - b
groovy> }
groovy> println 'fun1( 5, 2 )='+fun1( 5, 2 )
groovy> println "fun1( 'aabc', 'b' )="+fun1( 'aabc', 'b' )
groovy> println 'fun1( [1,2,3,4], [2,3] )='+fun1( [1,2,3,4], [2,3] )
fun1( 5, 2 )=3
fun1( 'aabc', 'b' )=aac
fun1( [1,2,3,4], [2,3] )=[1, 4]
А сегодня так на JS в другом проекте
> fun1 = function( a, b ){ return a - b }
[Function: fun1]
> fun1( 5, 2 )
3
> fun1( 'aabc', 'b' )
NaN
> fun1( [1,2,3,4], [2,3] )
NaN
И вот таких не совпадений типов данных может быть много и мне действительно надо знать особенности того или иного языка.Окей, я понимаю, что я сейчас выдумываю на ходу разные проблемы - но это не значит, что прям сейчас надо бросать известный вам язык и переходить на другой язык программирования.Речь о типах данныхВариантность как и ко/контр вариантность - это речь о типах данных и их отношениях между собой.Некоторые языки программирования создавались, чтобы избежать выше описанных проблем.Один из способов избежать - это введение системы типов данных.Вот пример на языке TypeScript
function sub( x : number, y : number ) {
return x - y;
}
console.log( sub(5,3) )
Этот код успешно скомпилируется в JS.А вот этот
function sub( x : number, y : number ) {
return x - y;
}
console.log( sub("aa","bb") )
Уже не скомпилируется - и это хорошо:
> tsc ./index.ts
index.ts:5:18 - error TS2345: Argument of type 'string' is not assignable
to parameter of type 'number'.
5 console.log( sub("aa","bb") )
~~~~
Found 1 error.
В примере выше функция sub требует принимать в качестве аргументов переменные определенного типа, не любые, а именно number.Контроль за типы данных я возлагаю уже компилятору TypeScript (tsc).ИнвариантностьРассмотрим пока понятие Инвариантность, согласно определениюИнвариа́нт — это свойство некоторого класса (множества) математических объектов, остающееся неизменным при преобразованиях определённого типа.Пусть A — множество и G — множество отображений из A в A. Отображение f из множества A в множество B называется инвариантом для G, если для любых a ∈ A и g ∈ G выполняется тождество f(a)=f(g(a)).Очень невнятное для не посвященных определение, давай те чуть проще:Инвариантность - это такое качество операций над данными, при котором тип данных в передаваемых в функцию и возвращаемый тип является один и тем же.Рассмотрим пример операции присвоения переменной, в JS допускается вот такой код
> fun1 = function( a, b, c ){
... let r = b;
... if( a ) r = c;
... return r + r;
... }
[Function: fun1]
> fun1( 1==1, 2, 3 )
6
> fun1( 1==1, "aa", "b" )
'bb'
> fun1( 1==1, 3, "b" )
'bb'
> fun1( 1!=1, 3, "b" )
6
> fun1( 1!=1, {x:1}, "b" )
'[object Object][object Object]'
В примере переменная r - может быть и типа string и number и объектом, со стороны интерпретатора сказать какого типа данных возвращает функция fun1 нельзя, пока не запустишь программу.Так же нельзя сказать какого типа будет переменная r. Тип результата и тип переменной r зависит от типов аргументов функции.Переменная r по факту может иметь два разных типа:
- В конструкции let r = b, переменная r будет иметь такой же тип, как и переменная b.
- В конструкции r = c, переменная r будет иметь такой же тип, как и переменная c.
В целом, такое не определенное поведение может сказаться на последующей логике поведения программы негативно.Можно наложить явным образом ограничения на вызов функции и проверять какого типа аргументы, например так:
> fun1 = function( a, b, c ){
... if( typeof(b)!=='number' )throw "argument b not number";
... if( typeof(c)!=='number' )throw "argument c not number";
... let r = b;
... if( a ) r = c;
... return r + r;
... }
[Function: fun1]
> fun1( true, 1, 2 )
4
> fun1( true, 'aa', 3 )
Thrown: 'argument b not number'
Это уже лучше, хоть об ошибке мы узнаем, во время выполнения, но она уже не приведет к негативным последствиям.Другой же аспект, в том что операция +, - и др… при операциях над числами - возвращают числа - это и есть инвариантность (в широком смысле), а вот при над числами и строками или различными типами данных - результат уже менее предсказуем.В языках со строгой типизацией операция конструкция let r = b и следующая за ней r = c не допустима, она может быть допустима если мы укажем типы аргументов.Пример Typescript:
function fun1( a:boolean, b:number, c:number ){
let r = b;
if( a ) r = c;
return r + r;
}
function fun2( a:boolean, b:number, c:string ){
let r = b;
if( a ) r = c;
return r + r;
}
И результат компиляции
> tsc ./index.ts
index.ts:9:13 - error TS2322: Type 'string' is not assignable to type 'number'.
9 if( a ) r = c;
~
Found 1 error.
Здесь в ошибки говориться явно, что переменная типа string не может быть присвоена переменной типа number.Вариантность - в компиляторах, это проверка допустимости присвоения переменной одного типа значения другого типа.Инвариантность - это такой случай, когда переменной одного типа присваивается (другая или эта же) переменная этого же типа.Теперь вернемся к строгому определению: выполняется тождество f(a)=f(g(a))То есть допустим у нас есть функции TypeScript:
function f(a:number) : number {
return a+a;
}
function g(a:number) : number {
return a;
}
console.log( f(1)===f(g(1)) )
Этот код - вот прям сторого соответствует определению.В контексте программирования Инвариантность - это не свойство значения функций, а соответствие типов данных, т.е. вот код ниже абсолютно валиден
function f(a:number) : number {
return a+a;
}
function g(a:number) : number {
return a-1;
}
let r = f(1)
r = f(g(1))
а такой код
function f(a:number) : number {
return a+a;
}
function g(a:number) : string {
return (a-1) + "";
}
let r = f(1)
r = f(g(1))
Уже невалиден (не корректен), так как:
- функция g возвращает тип string
- а функция f требует тип number в качестве аргумента
и вот такую ошибку обнаружит компилятор TypeScript.Первый итогВариантность и другие ее формы, как например Ин/Ко/Контр вариантность - это качество операции присвоения значения переменной или передачи аргументов в функцию, в которой проверяется типы данных передаваемых/принимаемых в функцию и переменную.Ко-вариантностьДля объяснения ко-вариантности и контр-вариантности, мне придется прибегнуть не к TypeScript, а к другому языку - Scala, причины я поясню ниже.Вы наверно уже слышали про ООП и наследование, про различные принципы SolidКо-вариантность обычно объясняют через наследование, и что наследуются все свойства и методы родительского класса - это верно, рассмотрим пару примеровКо-вариантность это такое качество операции присвоения значения переменной значение переменной другого типа, при котором сохраняются все свойства и операции. —–Есть несколько типов чисел и их можно расположить в следующей иерархии:
- Натуральные числа N
- N натуральные числа, включая ноль: {0, 1, 2, 3, … }
- N* натуральные числа без нуля: {1, 2, 3, … }
- Целые числа Z - обладают знаком (+/-) включают в себя натуральные
- Рациональные числа Q - дроби (два целых числа), включают в себя все бесконечное множество Z
- Вещественные числа R - это и рациональные и иррациональные числа (например ПИ, e, …)
- Комплексные числа C - числа вида a+bi, где a,b - вещественные числа, а i - мнимая единица
Давай те рассмотрим более подробно:Числа мы можем условно расположить согласно такой иерархии
- any - любой тип данных
- number - некое число
- int - целое число
- double - (приближенное) дробное число
- string - строка
так мы можем в языке TypeScript написать функции
function sum_of_int( a:int, b:int ) : int { return a+b; }
function sum_of_double( a:double, b:double ) : double { return a+b; }
function compare_equals( a:number, b:number ) : boolean { a==b }
в случае
let res1 : int = sum_of_int( 1, 2 )
Это будет случай инвариантного присваивания, т.к. типы полностью совпадают - результат вызова int, и переменная которая принимает результат то же int.Рассмотрим случай ко-вариантного присваивания
let res1 : number = sum_of_int( 1, 2 )
res1 = sum_of_double( 1.2, 2.3 )
В данном примере res1 - это тип number.В первом вызове res1 = sum_of_int( 1, 2 ), переменная res1 примет данные типа int, и это корректно, т.к. int это подтип number и по определению сохраняются все свойства и методы класса numberВо втором вызове res1 = sum_of_double( 1.2, 2.3 ) - переменная res1 примет данные типа double и это тоже корректно, так же по определениюО каких же операциях говорят что сохраняются? а все те же, мы все так же как и в первом, так и во втором случае можем выполнить операции проверки на равенство и д.р. для переменной res1:
let res1 : number = sum_of_int( 1, 2 )
let res2 : number = sum_of_doube( 1.2, 2.3 )
if( compare_equals(res1, res2) ){
...
}
ок, это работает, но компилятор нам нужен чтоб он за нас решал проблемы с типами, рассмотрим еще более “выпуклый” примерДопустим у нас есть фигуры: прямоугольник Box и круг Circle
class Box {
width : number
height : number
constructor( w: number, h: number ){
this.width = w;
this.height = h;
}
}
class Circle {
radius : number
constructor( r: number ){
this.radius = r
}
}
И нам надо подсчитать сумму площадей, прямоугольники можно хранить в одном массиве, а круги в другом
let boxs : Box[] = [ new Box(1,1), new Box(2,2) ]
let circles : Circle[] = [ new Circle(1), new Circle(2) ]
Мы напишем 2 функции по подсчету площади, одну для прямоугольников, другую для кругов
function areaOfBox( shape:Box ):number { return shape.width * shape.height }
function areaOfCircle( shape:Circle ):number { return shape.radius * shape.radius * Math.PI }
Тогда для подсчета общей суммы площадей код будет примерно таким:
boxs.map( areaOfBox ).reduce( (a,b,idx,arr)=>a+b ) +
circles.map( areaOfCircle ).reduce( (a,b,idx,arr)=>a+b )
Все выше выглядит ужасно, если вы знакомы с ООП или/и с базовой логикой (родовые, видовые понятия).Первое, что должно броситься в глаза - так это что свойство площадь применимо к обеим фигурам, а для подсчета суммы площадей, нет прямой необходимости как-то различать типы фигур.А по сему можно выделить общее абстрактное понятие Фигура и добавить в это абстрактное метод/свойство - area():number.
interface Shape {
area():number
}
Вторым шагом, это указать что классы Box и Circle реализуют интерфейс Shape, и перенести areaOfBox, areaOfCircle как реализацию area.
class Box implements Shape {
width : number
height : number
constructor( w: number, h: number ){
this.width = w;
this.height = h;
}
area():number {
return this.width * this.height
}
}
class Circle implements Shape {
radius : number
constructor( r: number ){
this.radius = r
}
area():number {
return this.radius * this.radius * Math.PI
}
}
Теперь нет необходимости разделять прямоугольники и круги в разные массивы, и писать сложный код
let shapes : Shape[] = [ new Box(1,1), new Box(2,2), new Circle(1), new Circle(2) ]
shapes.map( s => s.area() ).reduce( (a,b,idx,arr)=>a+b )
И в данном примере, ко-вариантность проявляется в инициализации массиваМассив определен как массив элементов типа Shape, мы инициализируем (т.е. присваиваем начальное значение) элементами другого типа под типа (Box, Circle).Ключевой момент в том, что Box и Circle реализуют необходимые свойства и методы которые требует интерфейс Shape.Компилятор отслеживает что присваиваемые значения реализуют заданное соглашение, т.е.Компилятор по факту отслеживает конструкцию let a = b, и возможны несколько сценариев:
- переменная a и b - одного типа, тогда инвариантная операция присвоения
- переменная a является базовым типом, а переменная b - подтипом переменной a - тогда ко-вариантная операция присвоения
- переменная a является подтипом переменной b, а переменная b - базовым (родительским) типом - тогда это контр-вариантная операция - и обычно компилятор блокирует такое поведение.
- между переменными a и b - нет общих связей - и тут компилятор блокирует то же поведение.
И вот пример, по пробуем добавить еще один класс который не реализует интерфейс Shape
class Foo {
}
let shapes : Shape[] = [ new Box(1,1), new Box(2,2), new Circle(1), new Circle(2), new Foo() ]
shapes.map( s => s.area() ).reduce( (a,b,idx,arr)=>a+b )
Результат компиляции - следующая ошибка:
> tsc index.ts
index.ts:31:84 - error TS2741: Property 'area' is missing in type 'Foo' but required in type 'Shape'.
31 let shapes : Shape[] = [ new Box(1,1), new Box(2,2), new Circle(1), new Circle(2), new Foo() ]
~~~~~~~~~
index.ts:2:5
2 area():number
~~~~~~~~~~~~~
'area' is declared here.
Found 1 error.
Для типа Foo не найдено свойство area, которое определенно в типе Shape.Тут уместно упомянуть о SOLIDL - LSP - Принцип подстановки Лисков (Liskov substitution principle): «объекты в программе должны быть заменяемыми на экземпляры их подтипов без изменения правильности выполнения программы». См. также контрактное программирование.Контр-вариантностьКонтр-вариантность, уже сложнее объяснить, для меня примеры с длегатами действовали на нервы, я же разобрался на примере с лямбд.В качестве примера, возьму язык Scala и подробно попытаюсь его разобрать:
package xyz.cofe.sample.inv
object App {
// Функция, на вход String, на выход Boolean, или кратко: (String)=>Boolean
def strCmp(a:String):Boolean = a.contains("1")
// Функция, на вход Int, на выход Boolean, или кратко: (Int)=>Boolean
def intCmp(a:Int):Boolean = a==1
// Функция, на вход String, на выход Boolean, или кратко: (Any)=>Boolean
def anyCmp(a:Any):Boolean = true
def main(args:Array[String]):Unit = {
// Инвариантное присвоение Boolean = Boolean
val call1 : Boolean = strCmp("a")
// Ко-вариантное присвоение Any = Boolean
val call2 : Any = strCmp("b")
// Инвариантное присвоение: (String)=>Boolean = (String)=>Boolean
val cmp1 : (String)=>Boolean = App.strCmp;
// Ко-вариантное присвоение (String)=>Boolean = (Any)=>Boolean
val cmp2 : (String)=>Boolean = App.anyCmp
// Инвариантное присвоение: (String)=>Boolean = (String)=>Boolean
val cmp3 : (Any)=>Boolean = App.anyCmp
// !!!!!!!!!!!!!!!!!!!!!!!
// Тут будет ошибка
// Контр-вариантное присвоение (Any)=>Boolean = (String)=>Boolean
val cmp4 : (Any)=>Boolean = App.strCmp
}
}
Что нужно знать о Scala:
- Тип Any - это базовый тип для всех типов данных
- Тип Int, Boolean, String - это подтипы Any
- Лямбды то же являются типами, в смысле типы их аргументов и результатов проверяет компилятор
- Тип лямбды записывается в следующей форме: (тип_аргументов,через_запятую)=>тип_результата
- Любой метод с легкостью преобразуется в лямбду переменная = класс.метод / переменная = объект.метод
- val в Scala, то же что и const в JS
В примере мы можем увидеть уже знакомыеИнвариантность в присвоении переменных:
// Инвариантное присвоение Boolean = Boolean
val call1 : Boolean = strCmp("a")
// Инвариантное присвоение: (String)=>Boolean = (String)=>Boolean
val cmp1 : (String)=>Boolean = App.strCmp;
cmp1 - это переменная содержащая лямбду, при том аргументы и результат которые заданы в определении типа лямбды, полностью совпадают с присваемым значением:
Ожидаемый тип (String)=>Boolean
Присваемый тип (String)=>Boolean
Ко-вариантность
// Ко-вариантное присвоение Any = Boolean
val call2 : Any = strCmp("b")
// Ко-вариантное присвоение (String)=>Boolean = (Any)=>Boolean
val cmp2 : (String)=>Boolean = App.anyCmp
Если в случае присвоения call2, тут все понятно, то может быть непонятно с cmp2.
Ожидаемый тип (String) => Boolean
Присваемый тип (Any) => Boolean
Внезапно отношение String -> к -> Any становится другим - контр-вариантным.В этом месте, уместно задаться WTF? - Все нормально!Рассмотрим функции выше
// Функция, на вход String, на выход Boolean, или кратко: (String)=>Boolean
def strCmp(a:String):Boolean = a.contains("1")
// Функция, на вход String, на выход Boolean, или кратко: (Any)=>Boolean
def anyCmp(a:Any):Boolean = true
При вызове cmp2( "abc" ) аргумент "abc" будет передан в anyCmp(a:Any), а по скольку String является под типом Any, то аргумент не дано преобразовывать и можно передать как есть.Иначе говоря вызов anyCmp( "string" ) и anyCmp( 1 ), anyCmp( true ) - со стороны проверки типов допустимы операции, по скольку
- принимаемые аргументы являются подтипами для принимающей стороны, тела функции
- тип принимаемого аргумента является родительским типом (надтипом) со стороны вызова функции
Т.е. можно при передаче аргументов, действуют ко-вариантность со стороны принимающей, а со стороны передающей контр-вариантность.Еще более наглядно это можно выразить стрелками:Операция присвоения должна быть ко-вариантна или инвариантна
assign a <- b
А операция вызова функции на оборот - контр-варианта или инвариантна
call a -> b
Этим правилом руководствуются многие компиляторы, и они определяют функции так:
- Операции передачи аргументов в функции по умолчанию являются контр-вариантны, со стороны вызова функции
- Операции присвоения результат вызова функции по умолчанию является ко-вариантны, со стороны вызова функции
Я для себя запомню так
Почему Scala, а не TypeScriptК моему удивлению TypeScript версии 4.2.4 не отрабатывает контр-вариантность в случае функций/лямбдВот мой исходник
interface Shape {
area():number
}
class Box implements Shape {
width : number
height : number
constructor( w: number, h: number ){
this.width = w;
this.height = h;
}
area():number {
return this.width * this.height
}
}
class Circle implements Shape {
radius : number
constructor( r: number ){
this.radius = r
}
area():number {
return this.radius * this.radius * Math.PI
}
}
class Foo {
}
const f1 : (number)=> boolean = a => true;
const f2 : (object)=> boolean = a => typeof(a)=='function';
const f3 : (any)=>boolean = f1;
const f4 : (number)=>boolean = f3;
const _f1 : (Box)=>boolean = a => true
const _f2 : (any)=>boolean = _f1
const _f3 : (Shape)=>boolean = _f1
В строке const f3 : (any)=>boolean = f1; и в const _f3 : (Shape)=>boolean = _f1 (а так же предыдущей) компилятор по моей логике должен был ругаться, но он этого не делал
user@user-Modern-14-A10RB:03:14:17:~/code/blog/itdocs/code-skill/types:
> ./node_modules/.bin/tsc -version
Version 4.2.4
user@user-Modern-14-A10RB:03:16:53:~/code/blog/itdocs/code-skill/types:
> ./node_modules/.bin/tsc --strictFunctionTypes index.ts
user@user-Modern-14-A10RB:03:18:26:~/code/blog/itdocs/code-skill/types:
> ./node_modules/.bin/tsc --alwaysStrict index.ts
user@user-Modern-14-A10RB:03:19:04:~/code/blog/itdocs/code-skill/types:
Потому пришлось взять язык с более жесткой проверкой типов, надеюсь в новых версиях исправят этот баг. Ко-вариантность/Контр-вариантность и типыЕще одна важная оговорка связанная с типами и ООП.Вариантность это не только про иерархию наследования!В примере о прямоугольнике и круге, я целенаправленно задействовал интерфейсы, хотя обычно используют общий базовый класс.Ко-Вариантность - это такое качество операции присвоения, когда целевой тип переменной совместим с исходным типом значения.Контр-вариантность - ровно та же ситуация с противоположным знаком.Тут надо дать пояснение слова совместимостьПример с кругами и прямоугольниками может быть написан на языке C или ассемблера, или JS ранних версий, в которых нет понятия классов, но при этом оно все так же будет работать.ООП с наследованием - это всего лишь способ, задать иерархию реальных типов объектов.В ряде языков был введен запрет на множественное наследование, и это я не могу назвать большим достижением, оно порождает проблемы.Например я могу выстроить разные наборы иерархий для одних и тех же сущностей:Например:
- Человек (общий класс)
- Национальность (под класс)
- Социальный статус (под класс)
или наоборот
- Человек (общий класс)
- Пол (под класс)
- Социальный статус (под класс)
Это я клоню к тому, что для одной и той же сущности может существовать множество способов квалификации.И один из подходов - эту сложную сушность (как например человек) можно рассматривать с различных сторон - и вот уже эти стороны можно выделить в виде интерфейсов.А уже в рамках того или иного интерфейса описывать интересующие свойства и методы для решения практических задач.Вариантность - это в первую очередь наличие интересующих зачем свойств/методов для наших задач. И это механизм контроля со стороны компилятора, для гарантии наличия этих свойств.Так, например тот или иной объект может быть не только каким либо под классом, но и реализовывать (через интерфейсы) интересующие нас свойства/методы - именно это я понимаю под словом совместимость.Далее можно вести разговор о множественном наследовании, трейтах, и прочих прелястях современных языков, но это уже выходит за рамки темы.
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка веб-сайтов, CSS, JavaScript, HTML, Node.JS] Как я сделал свою сборку Gulp для быстрой, лёгкой и приятной вёрстки
- [Программирование, Go, Микросервисы] Как писать кодогенераторы в Go
- [Разработка веб-сайтов, JavaScript, Программирование, Совершенный код] Погружение во внедрение зависимостей (DI), или как взломать Матрицу
- [Программирование, Julia, Искусственный интеллект, Data Engineering] Новая система автоматически очищает массивы ненадёжных данных (перевод)
- [Высокая производительность, PostgreSQL, Программирование, .NET, SQL] Как реляционная СУБД делает JOIN?
- [Веб-дизайн, Разработка веб-сайтов, Платежные системы, JavaScript, Дизайн мобильных приложений] Создаём королевскую форму для приёма банковских карт
- [Разработка веб-сайтов, JavaScript, Клиентская оптимизация, HTML, VueJS] Проблемы рендера 7-и тысяч элементов на Vuetify
- [Программирование, C++, C] Динамическая типизация C
- [Программирование, Управление проектами] Если у вас нашли SCRUM
- [Программирование, C++, Программирование микроконтроллеров] Работа с параметрами в EEPROM
Теги для поиска: #_javascript, #_programmirovanie (Программирование), #_scala, #_tipy_dannyh (типы данных), #_javascript, #_scala, #_typescript, #_javascript, #_programmirovanie (
Программирование
), #_scala
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:41
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Тема вариантов в программировании вызывает кучу сложностей в понимании, по мне это проблема в том, что в качестве объяснения берут не всегда успешные метафоры - контейнеры.Я надеюсь что может у меня получиться объяснить эту тему с другой стороны используя метафоры “присвоения” в разрезе лямбд.Зачем вообще эта вариантность нужна ?В целом без вариантности можно жить и спокойно программировать, это не такая уж архиважная тема, у нас есть множество примеров языков программирования в которых это качество не отображено.Ко-вариантность это о типах данных и их контроле со стороны компиляторов. И ровно с этого места надо откатиться и сказать о типах данных и зачем это нам нужно.Flashback к типамТипы данных сами по себе тоже не являются сверхважной темой, есть языки в которых тип данных не особенно нужны, например ассемблер, brainfuck, РЕФАЛ.В том же РЕФАЛ или ассемблере очень легко перепутать к кому типу относиться переменная, и очень легко, например можно допустить что из одной строки я вычту другую строку, просто опечатка, никакого злого умысла.В языках с поддержкой типов, компилятор увидел бы это опечатку и не дал бы мне скомпилировать программу, но… например JS > 'str-a' - 'str-b'
NaN >>> 'str-a' - 'str-b'
Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for -: 'str' and 'str' jshell> "str-a" - "str-b"
| Error: | bad operand types for binary operator '-' | first type: java.lang.String | second type: java.lang.String | "str-a" - "str-b" | ^---------------^ groovy> def fun1( a, b ){
groovy> return a - b groovy> } groovy> println 'fun1( 5, 2 )='+fun1( 5, 2 ) groovy> println "fun1( 'aabc', 'b' )="+fun1( 'aabc', 'b' ) groovy> println 'fun1( [1,2,3,4], [2,3] )='+fun1( [1,2,3,4], [2,3] ) fun1( 5, 2 )=3 fun1( 'aabc', 'b' )=aac fun1( [1,2,3,4], [2,3] )=[1, 4] > fun1 = function( a, b ){ return a - b }
[Function: fun1] > fun1( 5, 2 ) 3 > fun1( 'aabc', 'b' ) NaN > fun1( [1,2,3,4], [2,3] ) NaN function sub( x : number, y : number ) {
return x - y; } console.log( sub(5,3) ) function sub( x : number, y : number ) {
return x - y; } console.log( sub("aa","bb") ) > tsc ./index.ts
index.ts:5:18 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. 5 console.log( sub("aa","bb") ) ~~~~ Found 1 error. > fun1 = function( a, b, c ){
... let r = b; ... if( a ) r = c; ... return r + r; ... } [Function: fun1] > fun1( 1==1, 2, 3 ) 6 > fun1( 1==1, "aa", "b" ) 'bb' > fun1( 1==1, 3, "b" ) 'bb' > fun1( 1!=1, 3, "b" ) 6 > fun1( 1!=1, {x:1}, "b" ) '[object Object][object Object]'
> fun1 = function( a, b, c ){
... if( typeof(b)!=='number' )throw "argument b not number"; ... if( typeof(c)!=='number' )throw "argument c not number"; ... let r = b; ... if( a ) r = c; ... return r + r; ... } [Function: fun1] > fun1( true, 1, 2 ) 4 > fun1( true, 'aa', 3 ) Thrown: 'argument b not number' function fun1( a:boolean, b:number, c:number ){
let r = b; if( a ) r = c; return r + r; } function fun2( a:boolean, b:number, c:string ){ let r = b; if( a ) r = c; return r + r; } > tsc ./index.ts
index.ts:9:13 - error TS2322: Type 'string' is not assignable to type 'number'. 9 if( a ) r = c; ~ Found 1 error. function f(a:number) : number {
return a+a; } function g(a:number) : number { return a; } console.log( f(1)===f(g(1)) ) function f(a:number) : number {
return a+a; } function g(a:number) : number { return a-1; } let r = f(1) r = f(g(1)) function f(a:number) : number {
return a+a; } function g(a:number) : string { return (a-1) + ""; } let r = f(1) r = f(g(1))
function sum_of_int( a:int, b:int ) : int { return a+b; }
function sum_of_double( a:double, b:double ) : double { return a+b; } function compare_equals( a:number, b:number ) : boolean { a==b } let res1 : int = sum_of_int( 1, 2 )
let res1 : number = sum_of_int( 1, 2 )
res1 = sum_of_double( 1.2, 2.3 ) let res1 : number = sum_of_int( 1, 2 )
let res2 : number = sum_of_doube( 1.2, 2.3 ) if( compare_equals(res1, res2) ){ ... } class Box {
width : number height : number constructor( w: number, h: number ){ this.width = w; this.height = h; } } class Circle { radius : number constructor( r: number ){ this.radius = r } } let boxs : Box[] = [ new Box(1,1), new Box(2,2) ]
let circles : Circle[] = [ new Circle(1), new Circle(2) ] function areaOfBox( shape:Box ):number { return shape.width * shape.height }
function areaOfCircle( shape:Circle ):number { return shape.radius * shape.radius * Math.PI } boxs.map( areaOfBox ).reduce( (a,b,idx,arr)=>a+b ) +
circles.map( areaOfCircle ).reduce( (a,b,idx,arr)=>a+b ) interface Shape {
area():number } class Box implements Shape {
width : number height : number constructor( w: number, h: number ){ this.width = w; this.height = h; } area():number { return this.width * this.height } } class Circle implements Shape { radius : number constructor( r: number ){ this.radius = r } area():number { return this.radius * this.radius * Math.PI } } let shapes : Shape[] = [ new Box(1,1), new Box(2,2), new Circle(1), new Circle(2) ]
shapes.map( s => s.area() ).reduce( (a,b,idx,arr)=>a+b )
class Foo {
} let shapes : Shape[] = [ new Box(1,1), new Box(2,2), new Circle(1), new Circle(2), new Foo() ] shapes.map( s => s.area() ).reduce( (a,b,idx,arr)=>a+b ) > tsc index.ts
index.ts:31:84 - error TS2741: Property 'area' is missing in type 'Foo' but required in type 'Shape'. 31 let shapes : Shape[] = [ new Box(1,1), new Box(2,2), new Circle(1), new Circle(2), new Foo() ] ~~~~~~~~~ index.ts:2:5 2 area():number ~~~~~~~~~~~~~ 'area' is declared here. Found 1 error. package xyz.cofe.sample.inv
object App { // Функция, на вход String, на выход Boolean, или кратко: (String)=>Boolean def strCmp(a:String):Boolean = a.contains("1") // Функция, на вход Int, на выход Boolean, или кратко: (Int)=>Boolean def intCmp(a:Int):Boolean = a==1 // Функция, на вход String, на выход Boolean, или кратко: (Any)=>Boolean def anyCmp(a:Any):Boolean = true def main(args:Array[String]):Unit = { // Инвариантное присвоение Boolean = Boolean val call1 : Boolean = strCmp("a") // Ко-вариантное присвоение Any = Boolean val call2 : Any = strCmp("b") // Инвариантное присвоение: (String)=>Boolean = (String)=>Boolean val cmp1 : (String)=>Boolean = App.strCmp; // Ко-вариантное присвоение (String)=>Boolean = (Any)=>Boolean val cmp2 : (String)=>Boolean = App.anyCmp // Инвариантное присвоение: (String)=>Boolean = (String)=>Boolean val cmp3 : (Any)=>Boolean = App.anyCmp // !!!!!!!!!!!!!!!!!!!!!!! // Тут будет ошибка // Контр-вариантное присвоение (Any)=>Boolean = (String)=>Boolean val cmp4 : (Any)=>Boolean = App.strCmp } }
// Инвариантное присвоение Boolean = Boolean
val call1 : Boolean = strCmp("a") // Инвариантное присвоение: (String)=>Boolean = (String)=>Boolean val cmp1 : (String)=>Boolean = App.strCmp; Ожидаемый тип (String)=>Boolean
Присваемый тип (String)=>Boolean // Ко-вариантное присвоение Any = Boolean
val call2 : Any = strCmp("b") // Ко-вариантное присвоение (String)=>Boolean = (Any)=>Boolean val cmp2 : (String)=>Boolean = App.anyCmp Ожидаемый тип (String) => Boolean
Присваемый тип (Any) => Boolean // Функция, на вход String, на выход Boolean, или кратко: (String)=>Boolean
def strCmp(a:String):Boolean = a.contains("1") // Функция, на вход String, на выход Boolean, или кратко: (Any)=>Boolean def anyCmp(a:Any):Boolean = true
assign a <- b
call a -> b
Почему Scala, а не TypeScriptК моему удивлению TypeScript версии 4.2.4 не отрабатывает контр-вариантность в случае функций/лямбдВот мой исходник interface Shape {
area():number } class Box implements Shape { width : number height : number constructor( w: number, h: number ){ this.width = w; this.height = h; } area():number { return this.width * this.height } } class Circle implements Shape { radius : number constructor( r: number ){ this.radius = r } area():number { return this.radius * this.radius * Math.PI } } class Foo { } const f1 : (number)=> boolean = a => true; const f2 : (object)=> boolean = a => typeof(a)=='function'; const f3 : (any)=>boolean = f1; const f4 : (number)=>boolean = f3; const _f1 : (Box)=>boolean = a => true const _f2 : (any)=>boolean = _f1 const _f3 : (Shape)=>boolean = _f1 user@user-Modern-14-A10RB:03:14:17:~/code/blog/itdocs/code-skill/types:
> ./node_modules/.bin/tsc -version Version 4.2.4 user@user-Modern-14-A10RB:03:16:53:~/code/blog/itdocs/code-skill/types: > ./node_modules/.bin/tsc --strictFunctionTypes index.ts user@user-Modern-14-A10RB:03:18:26:~/code/blog/itdocs/code-skill/types: > ./node_modules/.bin/tsc --alwaysStrict index.ts user@user-Modern-14-A10RB:03:19:04:~/code/blog/itdocs/code-skill/types:
=========== Источник: habr.com =========== Похожие новости:
Программирование ), #_scala |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:41
Часовой пояс: UTC + 5