Русский
Русский
English
Статистика
Реклама

Scala

Ко-вариантность и типы данных

04.06.2021 02:06:03 | Автор: admin

Тема вариантов в программировании вызывает кучу сложностей в понимании, по мне это проблема в том, что в качестве объяснения берут не всегда успешные метафоры - контейнеры.

Я надеюсь что может у меня получиться объяснить эту тему с другой стороны используя метафоры присвоения в разрезе лямбд.

Зачем вообще эта вариантность нужна ?

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

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

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 - bgroovy> }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 )=3fun1( 'aabc', 'b' )=aacfun1( [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.tsindex.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

Ко-вариантностьобычно объясняют через наследование, и что наследуются все свойства и методы родительского класса - это верно, рассмотрим пару примеров


Ко-вариантностьэто такое качество операции присвоения значения переменной значение переменной другого типа, при котором сохраняются все свойства и операции.

Есть несколько типов чисел и их можно расположить в следующей иерархии:

  1. Натуральные числа N

    • N натуральные числа, включая ноль: {0, 1, 2, 3, }

    • N* натуральные числа без нуля: {1, 2, 3, }

  2. Целые числа Z - обладают знаком (+/-) включают в себя натуральные

  3. Рациональные числа Q - дроби (два целых числа), включают в себя все бесконечное множество Z

  4. Вещественные числа R - это и рациональные и иррациональные числа (например ПИ, e, )

  5. Комплексные числа 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, и возможны несколько сценариев:

  1. переменная a и b - одного типа, тогдаинвариантнаяоперация присвоения

  2. переменная a является базовым типом, а переменная b - подтипом переменной a - тогдако-вариантнаяоперация присвоения

  3. переменная a является подтипом переменной b, а переменная b - базовым (родительским) типом - тогда этоконтр-вариантнаяоперация - и обычно компилятор блокирует такое поведение.

  4. между переменными 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.tsindex.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.

Тут уместно упомянуть о SOLID

L - LSP - Принцип подстановки Лисков (Liskov substitution principle): объекты в программе должны быть заменяемыми на экземпляры их подтипов без изменения правильности выполнения программы. См. такжеконтрактное программирование.

Контр-вариантность

Контр-вариантность, уже сложнее объяснить, для меня примеры с длегатами действовали на нервы, я же разобрался на примере с лямбд.

В качестве примера, возьму язык Scala и подробно попытаюсь его разобрать:

package xyz.cofe.sample.invobject 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 = Booleanval call1 : Boolean = strCmp("a")// Инвариантное присвоение: (String)=>Boolean = (String)=>Booleanval cmp1 : (String)=>Boolean = App.strCmp;

cmp1 - это переменная содержащая лямбду, при том аргументы и результат которые заданы в определении типа лямбды, полностью совпадают с присваемым значением:

Ожидаемый тип  (String)=>BooleanПрисваемый тип (String)=>Boolean

Ко-вариантность

// Ко-вариантное присвоение Any = Booleanval call2 : Any = strCmp("b")// Ко-вариантное присвоение (String)=>Boolean = (Any)=>Booleanval 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)=>Booleandef 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 => trueconst _f2 : (any)=>boolean = _f1const _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 -versionVersion 4.2.4user@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 ранних версий, в которых нет понятия классов, но при этом оно все так же будет работать.

ООП с наследованием - это всего лишь способ, задать иерархию реальных типов объектов.

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

Например я могу выстроить разные наборы иерархий для одних и тех же сущностей:

Например:

  • Человек (общий класс)

    • Национальность (под класс)

      • Социальный статус (под класс)

или наоборот

  • Человек (общий класс)

    • Пол (под класс)

      • Социальный статус (под класс)

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

И один из подходов - эту сложную сушность (как например человек) можно рассматривать с различных сторон - и вот уже эти стороны можно выделить в виде интерфейсов.

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

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

Так, например тот или иной объект может быть не только каким либо под классом, но и реализовывать (через интерфейсы) интересующие нас свойства/методы - именно это я понимаю под словомсовместимость.

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

Подробнее..

Использование LoRa для интеграции кота в IoT

09.05.2021 16:23:04 | Автор: admin
Duivendrecht, вид на ферму и церковьDuivendrecht, вид на ферму и церковь

Я всегда мечтал жить в деревне - чтобы зелень и птички щебетали летом - но недалеко от города и выбора удобств. И наконец мечта сбылась - я поселился в доме с садом, в местечке Дёйвендрехт, тихой деревне, которая ближе к центру Амстердама чем половина собственных его районов.

А в дом с садом просто необходимы коты.

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

Поэтому через некоторое время у нас появился Барсик, по паспорту Эскобар.

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

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

Вариант превратить сад в клетку не устраивал никого. Вариант через некоторое время расклеивать объявления про потерянного кота, или же найти Барсика раскатанного по асфальту - тоже не казался слишком привлекательным.

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

Протестированы были устройства Invoxia, Findster, Tractive и некоторые другие. Invoxia это сеть SigFox, Tractive и прочие - GPRS с симкой, Findster - собственное радио.

  • Симочные все требуют денег, минимум 5 евро в месяц абонемент. При этом видимо используются какие-то дешёвые IoT симки с 2G connectivity. Задержки сигналов 1-2 минуты.

  • На SigFox возлагались надежды - но сеть теряет сигналы как-то уж очень часто, локации приходят в перемешанном порядке. Качество фикса позиции ужасное.

  • Findster - хороший фикс и реалтайм отслеживание. Производитель рекламирует радиус 900 метров в городе, реально 100+ метров - трекинг теряется. Его я использовал дольше всего - пока Барсик вокруг дома уже всё не изучил, ему не стало скучно и он отправился в более дальние экспедиции.

  • Жизнь батарейки - реалтайм GNSS трекинг выжирает часа за 2-3 батарейку у всех трекеров.

LoRa и The Things Network

Трекеру в общем-то не очень нужна симка, надо передавать десятки байт раз в минуту или несколько минут. Погуглив туда и сюда, выяснил, что есть радиостандарт LoRa, который гораздо лучше подходит под мой кейс чем всё вот это вот.

  • LoRa использует EU 868MHz нелицензированные частоты, которые доступны и в РФ.

  • LoRa использует chirp модуляцию, изначально придуманную для военных целей, которая позволяет энерго-эффективную радио коммуникацию в отсутствии прямой видимости.

  • LoRa поддерживает различные сети - можно делать частную сеть со своей базовой станцией, или пользоваться сетью провайдера. KPN предоставляет покрытие по всем Нидерландам.

  • Устройства LoRa не стоят индустриальных денег

Стандартных решений для трекеров с LoRa сетью на рынке не было - и нет - но почему бы и не сделать собственное?

Gateway и антенна


Нидерландская компания The Things Network предлагает TTN Indoor gateway за 70 евро. Установка и конфигурация (gateway передаёт через wifi на сеть TTN всё, что ловит на радио сам) была завершена за 10 минут.

TTN консоль сделана с любовью, всё понятно и удобно.TTN консоль сделана с любовью, всё понятно и удобно.

Единственная проблема - gateway содержит внутреннюю напечатанную антенну, которая не обеспечивает связи вне дома.

Решение проблемы - подключить внешнюю антенну. Благо дом свой, можно лезть на крышу и ставить что угодно.

Aurel GP 868 антенна на крышеAurel GP 868 антенна на крыше

Решение по частям

  • заказать внешнюю антенну с ground plane диаграммой (например Aurel GP 868, EUR 40,-)

  • заказать IPEX кабель-адаптер (для Aurel был нужен IPEX-to-BNC-female, EUR 3,-)

  • вскрыть корпус gateway, отсоединить IPEX кабель от печатной планы и воткнуть свой кабель внешней антенны

Наверное можно было сделать аккуратнее, но и так работает.Наверное можно было сделать аккуратнее, но и так работает.

После такого хака я увидел на гейтвее трафик с нескольких LoRa устройств поблизости. Это колхозный принцип организации TTN - каждый любитель, типа меня, с гейтвеем ловит и передаёт трафик от всех устройств, активированных в TTN. В сутки где-то 100 тысяч сообщений, но каждое меньше 100 байт, поэтому +10 мегабайт в сутки на фоне всего остального незаметно, от слова вообще.

В результате получается бесплатная (и без SLA) колхозная сеть. Покрытие не 100% и даже не близко, но сам факт, почему бы и нет?

Бесплатное покрытие TTN в АмстердамеБесплатное покрытие TTN в Амстердаме

С антенной как есть - покрытие порядка 1 км радиус, планирую поднять антенну на 6 метров, посмотрю насколько увеличится радиус. Фанаты устанавливают рекорды LoRa связи в несколько сотен километров.

На рынке LoRa трекеров есть несколько устройств, но единственное устройство можно было использовать в качестве кото-трекера. Это BroWAN Object Locator, которые делает тайваньская фирма Browan. Кроме этих сенсоров, они делают ещё десятки других LoRa устройств, от CO2 до протечек воды. Очень милые ребята, хорошая техническая поддержка.

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

BroWAN tabBroWAN tab

Вес 28 граммов, батарейка 540mAh, которой хватает на передачу позиции раз в минуту в течение 8 часов, или дольше, если реже.

Но если бы я был котом, мне бы с такой блямбой на шее бегать было бы неудобно. К тому же кот носил Findster и теперь носит два BroWAN tab - TTN с моей антенной и KPN, который тестируется.

Поэтому я купил шлейку для прогулок котов, отрезал всё лишнее и пришил как умел два кармашка, получился такой типа жилетик-разгрузка.

Пришлось ещё удлинить лямку вокруг шеи, чтобы не затягивать сильно, но и не расстёгивалось.

Два ярких вырвиглазных жёлтых кармашка - для оборудования. Вырвиглазных, потому что мех голубого британца Барсика - это наноматериал, который принимает разные оттенки серого в зависимости от угла падения света, смешивая кота с фоном. Сделать кота заметнее важно и для его безопасности, чтобы не раздавило машиной, и для птичек.

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

Чтобы сместить баланс жизни и смерти в пользу птичек, я ещё пришил звякающий шарик сверху. Теперь Эскобар может выполнять функцию верхнего хищника просто гоняя птичек, чтобы не жирели, но не убивая их. Или же, если некая птица не способна заметить кота с колокольчиком в ярком жилетики и свалить восвояси, кот может рассчитывать на применение по отношению к ней законов Дарвина, а не человеческой этики.

Ещё Барсик таскает в одном из кармашков маленькую круглую таблетку Tile- она не умеет в GNSS и из радио всего лишь Bluetooth. Но зато она умеет громко пиликать, когда ты от неё в 10 метрах или ближе (в рекламе 30-40, но с 10 точно берёт). Это позволяло мне находить жилетик в 6 случаях, когда кот его сбрасывал и гулял дальше голый.

Эскобар в боевом облаченииЭскобар в боевом облачении

Программное обеспечение

Пока что было всё про железки, теперь про софт немного. Архитектура решения выглядит примерно так:

Кот начинает гулять, трекер активируется акселерометром и начинает посылать позиции раз в минуту. Пакеты ловятся каким-то gateway (иногда несколькими) и пересылаются в сеть TTN.

Каждый пакет это примерно 50 байт, содержит заголовки, метаданные, локация и напряжение батарейки.

Приложение в консоли TTNПриложение в консоли TTN

Gateway добавляет свою информацию, в частности отношение сигнал/шум и посылает всё в сеть TTN. В консоли TTN каждое устройство (device) конфигурируется как честь приложения (application) - группа устройств, парсер входящих пакетов + дальнейшие интеграции - MQTT, HTTP и прочие.

Конфигурация устройстваКонфигурация устройства

В TTN application можно добавить функцию-парсер для преобразования байтов из устройства в структуру типа JSON. Для BroWAN трекеров код выглядит так:

function Decoder(bytes, port) {    var params = {        "bytes": bytes    };    bytes = bytes.slice(bytes.length-11);      if ((bytes[0] & 0x8) === 0) {        params.gnss_fix = true;      } else {        params.gnss_fix = false;      }      // Mask off enf of temp byte, RFU      temp = bytes[2] & 0x7f;      acc = bytes[10] >> 5;      acc = Math.pow(2, parseInt(acc) + 2);      // Mask off end of accuracy byte, so lon doesn't get affected      bytes[10] &= 0x1f;      if ((bytes[10] & (1 << 4)) !== 0) {        bytes[10] |= 0xe0;      }      // Mask off end of lat byte, RFU      bytes[6] &= 0x0f;      lat = bytes[6] << 24 | bytes[5] << 16 | bytes[4] << 8  | bytes[3];      lon = bytes[10] << 24 | bytes[9] << 16 | bytes[8] << 8  | bytes[7];      battery = bytes[1];      capacity = battery >> 4;      voltage = battery & 0x0f;      params.latitude = lat/1000000;      params.longitude = lon/1000000;      params.accuracy = acc;      params.temperature = temp - 32;      params.capacity = (capacity / 15) * 100;      params.voltage = (25 + voltage)/10;      params.port=port;      return params;}view rawttn-browan hosted with  by GitHub

В TTN добавлена интеграция HTTP, чтобы посылать события в приложение catracker, которое сохраняет позиции в базу и посылает их подключенным клиентам по вебсокетам.

Приложение состоит из Scala/Akka сервиса, фронтенда на голом TypeScript, Azure DevOps CI и Kubernetes дескриптора.

Полный код доступен в https://github.com/jacum/catracker.

Сегодня был дождь и Барсик не ходил далекоСегодня был дождь и Барсик не ходил далеко

Интерфейс минималистичный но вполне MVP - показывает проценты батарейки, позицию кота и время с апдейта последней позиции, если прошло больше двух минут. Скриншот сделан после 1 часа и 53 того, как кот пришёл домой - трекер не посылает событий, если акселерометр не показывает движения.

Большое спасибо TTN за надёжное и недорогое оборудование, и добротную консоль, и BroWAN за лучшие LoRa трекеты.

И конечно же коту Барсику за ежедневные усилия по тестированию решения.

Мяу!Мяу!

Оригинал (моей же) статьи

Подробнее..

IntelliJ IDEA 2020.3

28.12.2020 16:18:38 | Автор: admin
Привет Хабр!

Представляем последнее большое обновление IntelliJ IDEA в этом году. Версию 2020.3 можно скачать с нашего сайта, установить через приложение Toolbox, обновиться прямо в IDE или, если вы пользуетесь Ubuntu, с помощью snap-пакетов.


IntelliJ IDEA 2020.3 несет в себе множество полезных функций: интерактивные подсказки в отладчике, поддержку Git-стейджинга, расширенную поддержку записей и запечатанных классов из Java 15. В новой версии проще работать с окном Endpoints, фреймворками и профилировщиком. Мы также обновили начальный экран, улучшили сортировку вариантов автодополнения на основе машинного обучения и расширили возможности спелл-чекера.


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




Вот главные улучшения, вошедшие в версию 2020.3:


Редактор


  • Новые параметры переименования предлагают три опции: переименовать объект в комментариях, строках или в текстовых вхождениях.
  • Теперь можно переносить вкладки в разные области экрана и таким образом делить его по вертикали и по горизонтали. А с помощью действия Open in Right Split можно разделить редактор вертикально при открытии файла.
  • Закреплять вкладки стало проще: добавляйте файлы перетаскиванием. Также можно собрать все закрепленные вкладки в отдельном ряду.
  • Вы можете выбрать IntelliJ IDEA в качестве стандартного приложения для открытия файлов.
  • Теперь можно добавить шаблон, который создает сразу несколько файлов. Внутри шаблона вы можете ввести паттерн для создания имени файла и пути.
  • Мы улучшили форматирование Markdown, синхронизировали прокрутку превью и редактора, а также добавили поддержку Mermaid.js.

Взаимодействие с IDE


  • На начальном экране теперь четыре вкладки: для управления проектами, настройки интерфейса IDE, установки плагинов и быстрого доступа к справке и обучающим материалам.
  • Со вкладки Learn IntelliJ IDEA на экране приветствия можно перейти к интерактивным курсам, которые познакомят вас с возможностями IntelliJ IDEA на реальных примерах кода.
  • Теперь можно синхронизировать тему IDE с системными настройками.
  • Мы добавили новый режим чтения для файлов библиотек и файлов, предназначенных только для чтения. В таких файлах удобнее читать комментарии.
  • Чтобы открывать файлы в режиме LightEdit, используйте команду -e(edit). В окне LightEdit можно активировать режим IDE, чтобы использовать все функции IntelliJ IDEA.
  • При нажатии Alt+Enter IDE показывает варианты исправления ошибок правописания. Кроме того, для проверки стиля и грамматики мы начали использовать новую версию движка LanguageTool, который поддерживает более десятка новых языков.
  • В диалоге Search Everywhere можно искать Git-сообщения, теги и ветки, а также использовать его в качестве калькулятора.
  • Теперь по клику на файл его содержимое можно увидеть во вкладке предпросмотра.
  • IntelliJ IDEA сообщит вам о выходе обновления JDK и предложит его установить.
  • Мы добавили панель со смайлами для Linux.

Отладчик


  • В режиме отладки нажмите на переменную, чтобы получить подсказку с указанием связанных полей, значения которых можно изменить.
  • Мы добавили новый тип watch expressions, которые связаны с определенным контекстом и отображаются прямо в редакторе.
  • Во время работы отладчика доступны новые функции профилирования: Show referring objects и Calculate retained size.
  • Теперь на каждый сеанс отладки для задачи Gradle открывается только одна вкладка. В ней отображаются фреймы, переменные, а также вывод консоли.

VCS


  • В новой версии появилась поддержка Git-стейджинга. Теперь вы можете добавлять файлы на стейджинг прямо из IDE. В окне Commit вы увидите две новые секции Staged и Unstaged.
  • Меню VCS называется по имени системы контроля версий, которую вы используете. Еще мы убрали из него все действия, кроме самых актуальных.
  • IntelliJ IDEA автоматически исправляет недопустимые символы в именах веток. А в контекстном меню текущей ветки добавились новые связанные действия.

Java


  • IntelliJ IDEA сортирует варианты автодополнения на основе технологии машинного обучения.
  • Мы добавили новое действие для преобразования записей (records) в классы.
  • В этой версии анализ кода, рефакторинги и автодополнение поддерживают запечатанные классы.
  • Если в ваших файлах используется механизм шебанг, IntelliJ IDEA автоматически определит это и откроет их как надо.
  • Мы упростили извлечение Java-методов: IDE сразу же выполняет рефакторинг без промежуточных диалогов.
  • Добавили новые инспекции и intention-действия для Java, а также улучшили автодополнение.
  • Плагин для Lombok теперь встроен в IDE.

Совместная разработка


  • IntelliJ IDEA 2020.3 поддерживает Code With Me (EAP) наш новый сервис для парного программирования и совместной разработки.

Конфигурации запуска


  • Мы изменили интерфейс конфигураций запуска/отладки и разместили наиболее часто используемые настройки на одном экране. Кроме того, вы можете добавлять дополнительные параметры, если необходимо.

Профилировщик


  • Теперь в окне Profiler можно присоединить профилировщик к работающему приложению и быстро получить доступ к ранее созданным снэпшотам.
  • Открыть любой файл .jfr или .hprof теперь можно несколькими способами: перетащить его в редактор, использовать меню File | Open или дважды кликнуть по файлу на панели Project.

Поддержка фреймворков


  • В этой версии мы значительно улучшили окно Endpoints. Теперь в вы можете фильтровать результаты поиска веб-сервисов и удобно расположить их в IDE. Для каждого веб-сервиса есть доступ к документации, специальному HTTP-клиенту и Open API.
  • Можно экспортировать HTTP-запросы в cURL.
  • Автодополнение URL-адресов стало более информативным: отображаются иконки фреймворков, HTTP-методы и расположение исходных классов и файлов. URL-адреса, объявленные как deprecated, перечеркнуты.
  • Нажав на новый значок глобуса рядом с URL-адресом, вы быстро перейдете к доступным действиям.
  • Теперь анализ кода работает для Spring API: MVC Functional Routing, RestOperations, WebTestClient и Reactive WebClient.
  • HTTP-запросы в старом формате легко преобразовать в новый формат.
  • Мы улучшили анализ кода Swagger и добавили поддержку Swagger Hub.
  • При импорте Quarkus и Micronaut проектов автоматически создаются конфигурации запуска.
  • В IntelliJ IDEA работает автодополнение для имен методов-запросов в Micronaut Data репозиториях. Мы также добавили поддержку SQL и JPQL языков в аннотации Micronaut @Query.

Kubernetes


  • Вы можете загружать логи подов на свой компьютер и быстро удалять ресурсы Kubernetes.
  • Теперь можно автоматически загружать CRD-схемы из активного кластера.
  • Мы добавили действия Open Console и Run Shell.

Kotlin


  • Даты выхода обновлений плагина Kotlin теперь синхронизированы с выпуском новых версий IntelliJ IDEA.
  • Inline-рефакторинг возможен для элементов, объявленных в Java. При инлайне код автоматически конвертируется в Kotlin.
  • Также можно использовать inline-рефакторинг для элементов из библиотек с приложенными исходниками, в том числе для scope-функций also, let, run, with и apply.
  • При inline-рефакторинге улучшена обработка лямбда-выражений.
  • Мы добавили поддержку структурного поиска и замены (SSR) для Kotlin.

Инструменты для работы с базами данных


  • Теперь можно использовать SQL для запросов к MongoDB.
  • IntelliJ IDEA поддерживает сервис Couchbase Query.
  • Добавлены два новых формата экспорта: One-row и SQL-Insert-Multirow.

JavaScript


  • Мы интегрировали TypeScript language service с окном Problems и перенесли действия из окна TypeScript в специальный виджет в строке состояния.
  • Если у вас есть нереализованный React-компонент, IntelliJ IDEA создаст необходимую конструкцию кода за вас.
  • Теперь можно переходить к различным элементам JavaScript- и TypeScript-файлов с панели навигации.

Scala


  • Сервер компиляции Scala теперь компилирует независимые модули параллельно.
  • Мы добавили диаграммы компиляции, чтобы помочь вам оптимизировать структуру модулей проекта и параметры виртуальной машины на сервере компиляции.
  • Scala-плагин теперь может комбинировать префиксы пакетов IntelliJ IDEA с цепочками предложений пакетов и относительными импортами Scala.
  • Добавлена поддержка MUnit со всей привычной функциональностью.
  • Scala-плагин понимает новый синтаксис методов main.

Вот такой длинный список получился. Еще больше улучшений, вошедших в эту версию, вы найдете в примечаниях к релизу. Как и всегда, ждем ваших отзывов в комментариях и баг-репортов в нашем трекере.


Программируйте с удовольствием!

Подробнее..

Доказательное программирование

01.04.2021 22:22:46 | Автор: admin

Внимание!


  • Содержание данной статьи никак не связано с докладом академика А. П. Ершова "Научные основы доказательного программирования" 1984г.


  • Статья содержит утверждения, способные вызвать вспышки гнева и неконтролируемой агрессии. За последствия автор статьи ответственности не несет!


  • В тексте упоминаются следующие языки программирования: Java, Swift, Kotlin, Scala, Go, Haskell и др.


  • Эта статья антитезис. Автор ставит вопросы, но не считает своим долгом на все из них дать ответы.



В момент своего появления в Европе доказательная медицина казалась скандальной, неприятной и отвергаемой почти всем медицинским сообществом идеей. Даже в США, которые сейчас являются оплотом доказательной медицины, долгое время не хотели ее принимать. Основная идея докажи, что то, что ты собираешься сделать, реально поможет. Сейчас большинство назначений доктора делают исходя из приобретенных знаний и опыта. Но что если для определенных ситуаций можно создать такой протокол лечения, следуя которому с болезнью сможет справиться даже неспециалист, и будет доказано, что этот протокол работает? Можно ли покрыть такими протоколами все известные недуги? Все, конечно же, нет, но какие-то определенно, да.


И вот тут невольно возникает вопрос: не обошла ли медицина другую, казалось бы, не менее прогрессивную индустрию разработки программного обеспечения? Ок, у врачей есть этика, primum non nocere ("главное, не навреди"). Но разработчик не врач, и вроде как его это не касается или касается? Всегда ли разработчик выбирает лучшее решение для своего клиента? Не преследует ли время от времени разработчик какие-то свои цели (освоить новую технологию, попробовать новую либу, язык программирования и т.п.), о чем клиенту лучше не знать? Испытывает ли разработчик угрызения совести в связи с этим? Или это норма рынка? Ведь индустрия не стоит на месте, и если разработчик проигнорирует новомодное решение, то рискует не попасть на набирающий ход поезд под названием "мейнстрим"?


А надо ли вообще искать лучшее, оптимальное решение? Спрос на рынке программ растет как на дрожжах. Все больше устройств, которым необходимо собственное ПО. Эти программы надо создать, и у клиентов, скажем честно, не всегда есть выбор. В некоторых отраслях счет компаний, обладающих реальной экспертизой, идет на единицы. В такой ситуации разработчик диктует свои условия. Но даже там, где существует реальная конкуренция, всегда ли решение выбранного подрядчика обладает лучшим соотношением "цена качество"? Всегда ли разработчики сами могут это понять?


Давайте разбираться! На конкретных примерах.


"Чудесное" воскрешение Kotlin


Kotlin отечественный продукт, созданный в недрах небезызвестной российской компании JetBrains. Зачем действительно был создан этот язык, знает, видимо, только часть сотрудников этой компании, но можно пофантазировать. Какой программист не хочет создать свой язык? Вопрос риторический. В JetBrains тоже нашлись энтузиасты. Но мы знаем, что в этой компании проходимцев нет, все товарищи исключительно умные и образованные. В свои ряды они абы кого не берут. Если они создали язык и сказали, что язык крутой, и это то, что вам нужно, значит так оно и есть! Ребятам можно верить или нет?


Есть в JetBrains такой чувак (собирательный образ), который когда-то открыл для себя DSL. И в голову ему пришла "гениальная" идея, которая до этого, несомненно, не приходила в голову никому: не надо выбирать язык под задачу, вместо этого надо сделать язык для создания языков, и тогда разработчик сам будет создавать такой язык, который ему нужен для решения конкретной задачи! Бинго! Придумано сказано, сказано сделано. Сначала была статья, из которой уже было ясно, что перспектив у затеи немного, но это точно не было ясно чуваку из JetBrains. Если вы поняли, о каком продукте идет речь, то могли заметить: автор, задним числом все умные, а ты тогда это знал? Вы не поверите! На самом деле (философы, кстати, не любят эту фразу) это было бы понятно любому, кто читал анонс JetBrains и при этом имел достаточный опыт "полевой работы". Проблема кажется очевидной. Но не господам из знаменитой компании, которые уж точно знают, что программисту нужно, а что нет.


Любой, кто пробовал создать свой язык, осознает, что дело это не такое простое, как может показаться на первый взгляд. И вроде бы все инструменты есть, и книги написаны. Но вот что надо учесть: под ваш язык нет инфраструктуры, нет комьюнити, нет ничего! Вы создаете его в пустоте. Вы не найдете на рынке специалистов, знающих ваш язык. Всех надо готовить, всех надо учить. Не всем это понравится перспективы такого опыта с точки зрения рынка труда сомнительны.


Но чувак из JetBrains слишком умен, чтобы ошибаться. Он уж точно знает, что это взлетит. Создается команда разработчиков, и они начинают пилить очередной, но, конечно же, уникальный инструмент для создания языков программирования MPS. Прошло 10 лет. Если вы посещали конференции с участием JetBrains, то могли обратить внимание на изобилие продуктов, которые компания продвигает. На любой вкус. Только MPS вы там не увидите, т.к. он стыдливо спрятан в стол. Нет, продукт существует и по сей день. И даже есть одна, кажется европейская, компания, которая до сих пор им пользуется. Большая, надо сказать, компания. Может поэтому продукт еще жив.


Но чувак из JetBrains не успокаивается он делает выводы. К сожалению, не те, которые следовало бы. Гений решает впасть в другую крайность не менее гениальную, чем первая. Теперь, говорит он, я сделаю один язык, но по-гениальному универсальный. Который заменит все остальные. Потому что новый язык будет лучшим и самым правильным. В нем будет все удобно, все продумано. Гении иначе не умеют. Другие языки создавались лузерами. Наш их всех победит! Создается команда идейных разработчиков, и они начинают пилить очередной, но, конечно же, уникальный язык программирования, и гений из JetBrains с оптимизмом смотрит в будущее.


Язык почти готов, публикуются анонсы. И тишина. Вроде новый же язык! Почему за ним не выстроилась очередь?! Может дело в том, что примерно в это же время увесистую дверь комьюнити разработчиков с ноги открыла "всплывшая" невесть откуда Scala (о ней чуть позже), которая очаровала своими возможностями, и Kotlin растворился в сиянии новой игрушки программистов? Казалось, что новенький продукт JetBrains идет к катастрофе.


Но гений из JetBrains не может ошибаться! Он понимает, что Kotlin появился слишком рано, его время еще не пришло. В конце концов, когда разрабы наиграются со Scala, они оценят всю гениальность отечественного языка. У Kotlin еще все впереди! И он не ошибся. Помощь пришла откуда не ждали.


Изначально Kotlin эксплуатировал инфраструктуру Java, что позволило быстро вывести его на рынок. Java же известна не только как "банковский язык", на котором написано, пожалуй, большинство современных решений для этой отрасли, но и как основной язык разработки для самой популярной мобильной ОС. Главный ее конкурент ОС от Apple требовал от разработчиков использования мумии под названием Objective C. Изначальный выбор этого языка понятен, но гениев из Apple (а там такие тоже есть!) таки пристыдили достаточно, чтобы они выкатили комьюнити более современный язык. Swift выглядел очень модно! Особенно по сравнению с трупом, который приходилось использовать до этого. Google должна была ответить на вызов!


Так случилось, что Google к тому времени уже какое-то время сотрудничала с JetBrains, которая на базе замечательной (без шуток) Idea помогла запилить Anroid Studio не менее замечательный (тоже без шуток) инструмент! И вот так оказалось, что у JetBrains есть подходящий язык. И выглядит он, что характерно, модно, как и Swift. Java-то уж 30 лет! Старье! А тут такой красавец! Нужно было действовать решительно. В итоге Kotlin получил титул основного языка разработки приложений для Android. Вот он звездный час гения из JetBtains! Теперь-то разработчики от него никуда не денутся! Признание! Google переписывает все свои доки, дублируя примеры на отечественном языке программирования. Более того, примеры на Kotlin становятся основными. Разработчики IT-гиганта, попавшие под чары "нового" языка, начали строить планы по использованию встроенного в него конструктора DSL для Постойте! Что? Конструктор DSL? Встроен? Как? Опять?


История эта обрывается на настоящем моменте. В итоге миллионы мобильных разработчиков получили новый язык, на который многие из них начали переходить по настоянию Google. Что же дает нам этот чудесный образец творчества лучших инженеров из Санкт-Петербурга? Да примерно ничего! Точнее, шанс уйти в минус. Получить дополнительные затраты на создание и, что важно, на поддержку своих продуктов. Сам язык, если присмотреться, несет в себе больше проблем, чем решений. Почему? Да потому-что сложность разработки не уменьшается от замены языка на другой язык того же типа. Сложность разработки, как правило, заключается в самой задаче, которую надо решить. И, чем сложнее задача, тем сложнее и дороже ее решение. То, что на одном языке код выглядит немного короче, чем на другом, мало что меняет. Послушайте доклады инженеров из JetBrains. На что они делают упор? На то, что код на Kotlin будет "намного" меньше, чем, скажем, на Java. Только вот они как-то не задумываются над простым вопросом: а за чей счет банкет? За счет чего ушли те "лишние" строки кода Java? Не пропала ли из кода какая-нибудь полезная информация, которую раньше можно было бы просто прочесть, но теперь о ней надо догадываться по косвенным признакам? Не задумывались ли гении из JetBrains о том, что хитрый вывод типов, который проделывает созданный ими несомненно гениальный компилятор, нужно будет проделывать каждый раз и разработчикам, которые будут читать "пожатый" код, тратя на это лишнее время?


Опять вопросы Но хочется задать еще больше. Можем ли мы сравнить описанную ситуацию с выпуском нового лекарства на фармацевтический рынок? Лекарство должно пройти сертификацию прежде, чем его допустят к продаже в развитых странах. С точки зрения доказательной медицины, препараты с недоказанной эффективностью это мусор, который не имеет право называться лекарством. Другое дело, что большинство веществ никогда не проходило требуемую сейчас процедуру тестирования. Многие из них никогда это тестирование и не пройдут из-за его высокой стоимости. Но об этом все будут знать.


IT-рынок захлебывается в потоке новомодных языков, которые скоро перестанут друг от друга отличаться. Каждый новый язык по словам их создателей "уникален" и "современен". Приводится масса причин, почему разработчик должен выбрать именно этот язык. Но так ли обстоят дела, как то декларируется? Действительно ли новый язык, новая библиотека, новый фреймфорк, новая СУБД решат больше проблем, чем добавят? Будут ли они эффективней, чем ранее разработанные инструменты, которые уже используются и по которым уже есть какая-то экспертиза?


Пока отложим эти вопросы и двинемся дальше...


Свидетели Scala


Вы когда-нибудь общались с представителем "секты свидетелей" Scala? Он на вас будет смотреть свысока, даже если на 30 см ниже ростом. Потому что он познал. А вы не познали. Он "так может", а вы и "ваш язык" нет. За его запятой может прятаться вселенная, а за вашим "плюсиком" ну максимум конкатинация строк. Какая банальность! Его код это высокоинтеллектуальный ребус для таких же просветленных, как он сам. На самом деле это ребус даже для него самого, но он от этого лишь получает удовольствие. А вы со "своим языком" такого удовольствия лишены. Вы обречены писать банальности с использованием предопределенного набора банальных операторов. И символы, которые вы пишете, не имеют никакой дополнительной высокоинтеллектуальной нагрузки Кажется, что разработчики Scala попытались запихать в язык все, что смогли вспомнить. И если что-то не запихивалось, раздвигали и запихивали. Трудно сказать, чего там нет. И, казалось бы, вот Ну что еще нужно? Безграничные возможности для самовыражения!


Говорят, что "ажиотаж" вокруг Scala подутих в 2017-2018гг. Как по мне, ажиотажа, собственно, никогда и не было. Был интерес, было любопытство. Короткий период. Год-два. Но да, некоторые смельчаки не заметили подвоха и погрузились в Scala по пояс. Возможно, в их вселенной какой-то ажиотаж и был. Но большинство поматросило модную штучку и оставило в покое. А те, кто влез, те да Те влипли по-полной. Но признаться в этом себе невероятно сложно. Сложно признавать подобные ошибки. Лучше сказать, что язык замечательный, это программисты плохие. Не доросли. Не доучились. Низкий IQ. Язык для избранных. На конференции свидетель Scala показывает слайд с парой строк, выглядящих как обычное арифметическое выражение. Но по хитрому прищуру докладчика все понимают, что здесь есть второе дно. И что эти две строки надо "читать". И дальше он с упоением рассказывает, как именно. Почему он так сделал? Потому что может! Потому что язык так может.


Согласно индексу TIOBE за март 2021, Scala и Kotlin находятся на соседних строчках. Считаю, замечательное соседство! Индустрия в безопасности, ее иммунная система справилась с недугом и миновала пропасть, по краю которой прошла. Да, есть потери. Scala поглотила слабых духом и оставила за собой шлейф из начатых проектов, от которых не просто будет избавиться. Но ничего. Поглощенным поможет реабилитация, а проекты рано или поздно умрут своей смертью. Все как обычно. Все имеет начало и конец.


А есть ли что-то подобное в фарме? Когда-то фенобарбитал можно было купить без рецепта, сейчас же во многих странах его вообще нет запретили (основное действующее вещество "Корвалола"). Стволовые клетки колют как витамины, но уже многие знают, чем это может обернуться. В 30-е годы прошлого века Кюри толкали свою радиоактивную косметику "Tho-Radia" в итоге она была признана "ядовитой". Детокс-терапия клизма, диета, "очищение" очень модно (не запрещено)!


Сейчас у общества есть защитный механизм доказательная медицина. В каком-то смысле это заклинание позволяет уберечься от подобной "скверны". В конце концов, всегда можно выяснить, была ли доказана эффективность того или иного препарата. Если не получается, есть комьюнити врачей, которые помогут.


В индустрии разработки пока ничего такого нет. Нам выкатывают нечто, что выглядит соблазнительно, и в итоге, в лучшем случае с небольшими потерями, мы проходим сложный период, движемся дальше. Но так бывает не всегда. Мириады неоновых вывесок с рекламой самых новых технологий и решений обволакивают неподготовленный ум начинающего программиста и запускают свои щупальца в его новые проекты. Да и опытный программист может быть усыплен очаровательным пением коварных IT-сирен. Все ли из того, что вы задействовали в своем проекте, вам действительно нужно? Или это дань моде, когда страх "отстать" не оставляет вам выбора? Действительно ли ваш выбор продиктован личным опытом? Возможно ли иметь качественный личный опыт, например на рынке разработки мобильных приложений, когда Google меняет правила игры раз в несколько месяцев?


Ну вот, опять! Вопросы, вопросы...


Сверхновый GO


Вот уж где создатели языка определенно постарались. Go, по сравнению с Kotlin, это действительно большая работа. За простотой синтаксиса скрывается целая сеть сложных компромиссов. Несомненно, назвать этот язык чьим-то клоном никак нельзя. Современный, продуманный, легко читаемый. Последнее, при таком синтаксическом аскетизме, кажется невероятным достижением разработчиков. Чудо чудесное, не иначе!


Не слишком ли все хорошо? Что-то здесь не так, что-то мы упускаем, что-то ускользает от нашего взора. Вот! Кажется я вижу чьи-то торчащие уши! Взять тесты. Почему здесь так много комментариев? Как это? Они интерпретируются? Это что, значит, в комментах спрятан еще один язык? Дополнительный синтаксис, которого как бы нет? Но он как бы не подходит под утверждение "ничего лишнего", так что там ему и место нечего портить стройный язык всякой второстепенной чепухой! На самом деле (опять эта нефилософская фразочка) тут нет вины разработчиков: причина такого решения в самой природе тестов и общей проблеме всех современных мейнстримовых языков. Но это тема отдельной статьи. Сейчас не об этом. Так что здесь я разработчиков не виню они сделали все, как принято. Хотя осадочек таки есть Эх!


Легенда гласит, что инженеры Google создавали язык для внутренних нужд и что он должен был быть достаточно простым и доступным даже слесарям из хозгруппы. Если это так, то многое становится понятным. Иначе сложно представить, что еще могло заставить разработчиков языка так поступить с одной из базовых концепций ООП. Когда я приношу домой новый телевизор, к нему прилагается пульт. Когда я сажусь на водительское место в авто, там уже есть органы управления. Я не ношу их с собой, как и не покупаю телевизор к уже имеющемуся у меня пульту. К новому телефону продается чехол, который выпускают специально под данный девайс, а не наоборот. Что же с инженерами Google не так? Почему они делают это с нами? Где чертов пульт?!


Если же отбросить сие недоразумение с интерфейсами, то в целом язык следует основным канонам и придраться тут не к чему. Но, раз уж мы затронули эту тему, пару слов про ООП-диссидентов. Сложно сказать, что движет этими несчастными. Возможно, это защитный механизм, позволяющий не сойти с ума, когда единственным рабочим языком программирования в арсенале является JavaScript? А может, это патологическая неспособность к абстрактному мышлению? Или отсутствие опыта работы над по-настоящему большими проектами? Чье-то тлетворное влияние? Или это скрытая сверхспособность помнить весь проект в деталях независимо от того, какого он размера, и никогда не допускать ошибок при взаимодействии между компонентами системы? Кто знает, кто знает Больно уж много специалистов с этой сверхспособностью развелось...


Что же подсказывает нам доказательная медицина? Следует ли нам избавиться от традиционных методов лечения, когда врач изучает все, что касается человеческого организма (анатомию, физиологию, биохимию), и при этом знает лекарственные вещества, оставить хирургов, а обычных терапевтов заменить кабинками "Instant Doctor", которые будут следовать доказанному протоколу лечения и использовать только средства с доказанной эффективностью? Насколько все можно упростить? Реальность такова, что доказательство эффективности различных химических веществ мероприятие крайне недешевое и доступно только Большой Фарме. А те не будут тратить деньги на лекарства, на которых они не смогут сорвать куш. Так что большинство лекарств существующему стандарту эффективности не будут соответствовать никогда. Эффективность парацетамола не доказана, и никто не знает, почему он делает то, что делает. Но помогает же!


Разработчики Kotlin забыли (или никогда не знали), почему в Java нет перегрузки операторов. Создатели Scala вообще об этом не задумывались. А инженеры из Google по какой-то причине решили, что их интерпретация основ ООП поистине революционна. Это те самые случаи, когда новым выглядит хорошо забытое старое. Смущает то, что нет какого-либо серьезного анализа этих языков, наоборот Интернет переполнен исключительно хвалебными отзывами. Т.е. ни о попытке как-то подтвердить эффективность навязываемого инструмента, ни и о банальной критике этих решений и речи нет. В чем причина?


Можно взглянуть на все с другой стороны. Не нравится, не ешь. Дай рынку насытиться! Изобилие это хорошо! Есть конкуренция, есть выбор. И кажется, что все так, выбор есть, но можем ли мы правильно выбрать? И всегда ли этот выбор действительно есть? Какова цена ошибки? Каким требованиям должен соответствовать язык программирования в вашем случае? Каким стандартам соответствует выбранный язык? Только тем, которые сформулировали сами создатели языка? Очень надежно! Или он просто вам понравился? Вы хотите его изучить, потому что коллеги о нем все время говорят, хвалят и все такое? А знает ли об этом ваш заказчик? Готов ли он оплачивать ваше обучение?


Вы выбрали новую СУБД, которая только появилась на рынке. Но исследовал ли кто-то ее надежность? Кто авторитетно может это исследование подтвердить? Вы? Ваш коллега? Он знает, как проводить такие исследования? Точно? Или исследование будет перенесено в продакшн и, конечно, оплачено ничего не подозревающим клиентом? Не стоит ли вспомнить здесь об этике? Ведь в данный момент для этого клиента вы олицетворяете все программистское сообщество, и если дискредитируете свою работу, то вы дискредитируете тем самым все сообщество, перекладывая стоимость своих ошибок на него, как это делают создатели разного рода новых инструментов и языков программирования. Без контроля, без сертификации, под честное слово.


Ну и напоследок...


Альтернативная медицина


Опять провал, опять ничего не выходит. Ошибки лезут из всех щелей. Паника. Что же делать? Как все исправить? Как сделать так, чтобы все работало с первого раза, а объем проблем перестал нарастать, словно снежный ком? Надо начать все сначала, время еще есть, но где гарантия, что результат будет другим?.. Что это за сладкий шепот в левом ухе? Кажется, он что-то пытается сказать! Шепот: "Функциональный язык". Что? Шепот: "Выбери функциональный язык забей на постылую императивщину, пора сменить парадигму". О чем это он? Ну-ка ну-ка, Интернет Так-так-так. Ого! Какая интересная концепция! Шепот, где же ты был раньше? Наконец-то решение найдено, теперь все заработает с пол-оборота! Но что-то терзает душу молодого разработчика, что-то не дает покоя что же опять не так?


Мы нашли вашу кучу, проектов на Haskell в ней не было.


"Если все так чудесно, размышляет наш герой, где те мириады проектов, написанных на функциональных языках программирования? Почему большинство программистов продолжают поедать мейнстримовый кактус? Возможно, от нас что-то скрывают? Тайный орден охраняет секреты эффективного программирования? Не просто, наверное, думает он, скрывать секреты, связанные с идеей, возраст которой сопоставим с возрастом самой отрасли. Или тут какой-то подвох? Может, все дело в системе образования, когда в учебном плане по информатике доминирует императивный подход?"


Может ли разработчик разобраться в этом вопросе, не погружаясь в него с головой и не расчищая авгиевы конюшни от "авторитетных" мнений "специалистов"? Что он обнаружит, попытавшись найти необходимый источник информации? Холивар, когда оппоненты до последнего готовы отстаивать свое мировоззрение, свой комфортный мирок, который они с таким трудом выстроили из скудной смеси парадигм и традиций и который готов рассыпаться на части от любой крамольной мысли? Сможет ли найти наш бедолага что-то, что укажет нужное именно ему направление? Какой-нибудь путеводитель, учебник. Или без наставника здесь не обойтись?


А верно ли вообще ставить вопрос таким образом? Действительно ли есть из чего выбирать? Возьмем функциональный Haskell, который позиционируется как язык общего назначения. Что бы это ни значило, тот же самый ярлык несут на себе такие монстры, как C++, Java, С#. Может ли Haskell составить им конкуренцию на этом поле? Вот реально? В теории несомненно. Но на практике, как говорится, не тратьте свое время. Хотя такой язык, несомненно, достоин того, чтобы с ним познакомиться в свободное от работы время. Что же с ним не так?


Придется совершить каминг-аут: я обожаю Prolog! Как и Haskell, он относится к семейству декларативных языков. Это когда вы не говорите машине, как именно нужно что-то сделать, не указываете последовательность действий, которые следует совершить, а устанавливаете некие границы и формулируете задачу, т.е. что нужно сделать, при этом на вопрос как пытается ответить уже сам компилятор. И это, казалось бы, хорошо. Но все меняется в тот момент, когда программа начинает делать что-то не то. Не то, что от нее ожидали. Возникает закономерное желание понять: почему она поступила именно так? И вот здесь выясняется, что любой императивный язык буквально "ткнет пальцем" в место в коде, где проявилась проблема и откуда ее можно локализовать. С декларативными языками иначе. Там нет привычного нам стека выполнения, который однозначно бы мапился на код. В случае с Prolog он может совершить миллион сопоставлений, прежде чем ошибка даст о себе знать. Это как если бы ваш стектрейс состоял из миллиона строк и в какой-то одной из этих строк что-то пошло не так, что-то не сошлось, какое-то правило не сработало.


Если учесть, что даже вооружившись самыми передовыми средствами отладки, найти проблему в императивном коде бывает ой как не просто, легко понять, почему в реальной жизни программисты предпочитают не связываться с языками, где отладка пусть и немного, но сложнее. Да, императивный код частенько "отрывается" от постановки задачи, когда приходится искать, где именно в коде реализована конкретная функция, гадать, почему разработчик выбрал именно такое решение и действительно ли оно корректно. Но, видимо, по соотношению всех плюсов и минусов императивные языки имеют серьезный гандикап.


Как же ориентироваться в этом океане технологий? Может, есть в нем тихие гавани, которые помогут найти решение? Взять HTML. Это не просто язык, это промышленный аппликационный стандарт. Т.е. речь не о стандарте, описывающем сам язык, а о стандарте для браузеров, которые обязаны его поддерживать. Ни анархии тебе, ни конкуренции. И кажется, что всех все устраивает. Его неизменный попутчик JavaScript при всей своей убогости тоже стандарт; лишь 10 лет назад разработчики браузеров договорились о том, чтобы заложить первый кирпич в фундамент куда более продуманной технологии WebAssembly. Чуть хуже дела обстоят в мобильной разработке. Тут таких стандартов, к сожалению, нет. Хотя попытки их внедрить таки имеют место быть. От унылой Cordova, до новенького Flutter. Но без поддержки всех участников рынка эти попытки обречены. Отрасль, скорее, идет в обратную сторону: Huawei выкатывает свою собственную ОС, и, учитывая размер рынка Китая, игнорировать ее, похоже, не получится.


Разработчики же прочих платформ в большинстве своем не накладывают никаких ограничений на используемые технологии (Apple не в счет), но и не делают попыток о чем-то договориться. Пожалуй, Microsoft достигла наибольших успехов с C#, рождение которого стало итогом войны с Sun Microsystems за право вносить изменения в стандарт Java. Microsoft добилась впечатляющих успехов в продвижении своего детища, но так и не смогла сдвинуть Java с ее пьедестала. Может, в будущем? Кто знает.


Способно ли развитие стандартизации изменить ситуацию? Или не стоит ничего менять, и так хорошо? Или не хорошо? Кажется, что спрос есть. Пример EJB, который канул в лету вместе с Sun, показывает, что на самом деле (вот опять) движение в эту сторону возможно и рынок готов его поддержать. Но неудача EJB могла быть неверно интерпретирована, что и привело к топтанию на месте. А может быть, у затеи изначально не было шансов? Как бы то ни было, при всем разнообразии решений то и дело вываливающихся на и без того переполненный рынок, кажется, что ситуация напоминает настоящий застой. Парадокс или кризис? Или все ок, и думать тут особо не о чем? Как воспринимать все это изобилие? Как новые игрушки, с которыми интересно повозиться (за чей-то счет, естественно), или как пилюли, эффективность которых должна быть подтверждена? Как воспринимать эти "игрушки" в руках коллег (в широком смысле), результаты игр которых разгребать потом придется вам? Может быть, новая этика?


Вопросы, вопросы, вопросы...


Я все сказал
Подробнее..

Перевод Языки любимые и языки страшные. Зелёные пастбища и коричневые поля

07.05.2021 14:19:42 | Автор: admin


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

В опросах есть категории Самые страшные языки программирования (The Most Dreaded Programming Languages) и Самые любимые языки. Оба рейтинга составлены на основе одного вопроса:

На каких языках вы провели обширную работу по разработке за последний год, и на каких хотите работать в следующем году? (Если вы работаете с определённым языком и намерены продолжать это делать, пожалуйста, установите оба флажка).

Страшный язык это такой, с которым вы активно работаете в текущем году, но не хотите продолжать его использовать. Любимый язык тот, который вы широко используете и хотите продолжать использовать. Результаты интересны тем, что отражают мнения людей, которые активно используют каждый язык. Не учитываются мнения типа Я слышал, что Х это круто, когда люди высоко оценивают вещи, которые они НЕ используют, потому что они слышали, что это новый тренд. Обратное тоже правда: люди, которые выражают отвращение к какому-то языку, реально широко используют его. Они боятся языка не потому, что слышали о его сложности, а потому, что им приходится работать с ним и испытывать настоящую боль.

Топ-15 страшных языков программирования:
VBA, Objective-C, Perl, Assembly, C, PHP, Ruby, C++, Java, R, Haskell, Scala, HTML, Shell и SQL.

Топ-15 любимых языков программирования:
Rust, TypeScript, Python, Kotlin, Go, Julia, Dart, C#, Swift, JavaScript, SQL, Shell, HTML, Scala и Haskell.

В списке есть закономерность. Заметили?

Худший код тот, что написан до меня


Старый код хуже всего. Если кодовая база в активной разработке более трёх лет, то она уже непоследовательная. На простой первый слой накладываются особые случаи и оптимизация производительности, а также различные ветви, управляемые параметрами конфигурации. Реальный код эволюционирует, чтобы соответствовать своей нише, одновременно он становится всё сложнее и труднее для понимания. Причина проста, и я впервые услышал эту фразу от Джоэла Спольски.

Причина, по которой [разработчики] считают старый код бардаком, заключается в кардинальном, фундаментальном законе программирования: читать код труднее, чем писать его.

Джоэл Спольски Грабли, на которые не стоит наступать

Назовём это Законом Джоэла. Из этой посылки вытекает многое. Почему большинство разработчиков думают, что унаследованный ими код это бардак, и хотят выбросить его и начать всё сначала? Потому что написание чего-то нового проще для мозга, чем тяжёлая работа по пониманию существующей кодовой базы, по крайней мере, на начальном этапе. Почему попытки переписать код часто обречены на провал? Потому что многие мусорные артефакты это жизненно важные небольшие улучшения, которые накапливаются с течением времени. Без какого-то конкретного плана по рефакторингу вы в конечном итоге вернётесь к тому, с чего начали.


Scott Adams Understood

Легко понять код, который вы пишете. Вы его выполняете и совершенствуете по ходу дела. Но трудно понять код, просто прочитав его постфактум. Если вы вернётесь к своему же старому коду то можете обнаружить, что он непоследовательный. Возможно, вы выросли как разработчик и сегодня бы написали лучше. Но есть вероятность, что код сложен по своей сути и вы интерпретируете свою боль от понимания этой сложности как проблему качества кода. Может, именно поэтому постоянно растёт объём нерассмотренных PR? Ревью пул-реквестов работа только на чтение, и её трудно сделать хорошо, когда в голове ещё нет рабочей модели кода.

Вот почему вы их боитесь


Если реальный старый код незаслуженно считают бардаком, то может и языки программирования несправедливо оцениваются? Если вы пишете новый код на Go, но должны поддерживать обширную 20-летнюю кодовую базу C++, то способны ли справедливо их ранжировать? Думаю, именно это на самом деле измеряет опрос: страшные языки, вероятно, будут использоваться в существующих проектах на коричневом поле. Любимые языки чаще используются в новых проектах по созданию зелёных пастбищ. Давайте проверим это.1

Сравнение зелёных и коричневых языков


Индекс TIOBE измеряет количество квалифицированных инженеров, курсов и рабочих мест по всему миру для языков программирования. Вероятно, есть некоторые проблемы в методологии, но она достаточно точна для наших целей. Мы используем индекс TIOBE за июль 2016 года, самый старый из доступных в Wayback Machine, в качестве прокси для определения языков, накопивших много кода. Если язык был популярным в 2016 году, скорее всего, люди поддерживают написанный на нём код.

Топ-20 языков программирования в списке TIOBE по состоянию на июль 2016 года: Java, C, C++, Python, C#, PHP, JavaScript, VB.NET, Perl, ассемблер, Ruby, Pascal, Swift, Objective-C, MATLAB, R, SQL, COBOL и Groovy. Можем использовать это в качестве нашего списка языков, которые с большей вероятностью будут использоваться в проектах по поддержке кода. Назовём их коричневыми языками. Языки, не вошедшие в топ-20 в 2016 году, с большей вероятностью будут использоваться в новых проектах. Это зелёные языки.


Из 22 языков в объединённом списке страшных/любимых 63% коричневых

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

Java, C, C++, C#, Python, PHP, JavaScript, Swift, Perl, Ruby, Assembly, R, Objective-C, SQL


Зелёный язык: язык, который вы с большей вероятностью будете использовать в новом проекте.

Go, Rust, TypeScript, Kotlin, Julia, Dart, Scala и Haskell

У TIOBE и StackOverflow разные представления о том, что такое язык программирования. Чтобы преодолеть это, мы должны нормализовать два списка, удалив HTML/CSS, шелл-скрипты и VBA.2

Конечно, простое деление на зелёные и коричневые упускает много нюансов, в том числе по размеру полей. Я ожидаю, что больше зелёных пастбищ должно быть на Swift, чем на Objective-C, но и нынешняя методика, кажется, охватывает всё, что нам нужно. В этом списке гораздо больше коричневых языков, чем зелёных, но это вполне ожидаемо, ведь новых языков ежегодно появляется относительно немного.

Теперь можно ответить на вопрос: люди действительно боятся языков или же они просто боятся старого кода? Или скажем иначе: если бы Java и Ruby появились сегодня, без груды старых приложений Rails и старых корпоративных Java-приложений для поддержки, их всё ещё боялись бы? Или они с большей вероятностью появились бы в списке любимых?

Страшные коричневые языки



Страшные языки на 83% коричневые

Топ страшных языков почти полностью коричневый: на 83%. Это более высокий показатель, чем 68% коричневых языков в полном списке.

Любимые зелёные языки



Любимые языки на 54% зелёные

Среди любимых языков 54% зелёных. В то же время в полном списке всего лишь 36% языков являются зелёными. И каждый зелёный язык есть где-то в списке любимых.

Ещё один недостаток человеческого характера заключается в том, что все хотят строить и никто не хочет заниматься обслуживанием.

Курт Воннегут

Этого недостаточно, чтобы с уверенностью сказать, что необходимость использовать язык в проекте поддержки внушает страх. Но очень похоже, что это как минимум одна из причин. Многие из любимых языков слишком новы или были слишком непопулярны, чтобы накопить много больших и грязных проектов.

Другими словами, Rust, Kotlin и другие зелёные языки пока находятся на этапе медового месяца. Любовь к ним может объясняться тем, что программистам не надо разбираться с 20-летними кодовыми базами.

Устранение предвзятости




Некоторые новые или исторически менее популярные языки программирования могут быть лучше, чем старые или более распространённые языки, но наша способность судить о них кажется довольно предвзятой. В частности, если язык новый или ранее не использовался, то у него некий ангельский образ. А чем дольше используется язык, тем более дьявольский лик он приобретает в глазах разработчиков. Думаю, причина в том, что никому не нравится поддерживать чужой код. А также из-за Закона Джоэла: читать в реальном мире очень сложно. Создавать что-то новое вот это весело, а для этого чаще используются новые языки.

Цикл хайпа языков программирования


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


Цикл хайпа языков программирования

У меня под рукой нет данных, но я отчётливо помню, что Ruby был самым популярным языком в 2007 году. И хотя сегодня у него больше конкурентов, но сегодня Ruby лучше, чем тогда. Однако теперь его боятся. Мне кажется, теперь у людей на руках появились 14-летние приложения Rails, которые нужно поддерживать. Это сильно уменьшает привлекательность Ruby по сравнению с временами, когда были одни только новые проекты. Так что берегитесь, Rust, Kotlin, Julia и Go: в конце концов, вы тоже лишитесь своих ангельских крылышек.3



1. Сначала я придумал критерии. Я не искал данных, подтверждающих первоначальную идею.

Была мысль определять статус зелёного или коричневого по дате создания языка, но некоторые старые языки нашли применение только относительно недавно.

Вот методика измерения TIOBE, а их исторические данные доступны только платным подписчикам, поэтому Wayback Machine. [вернуться]

2. HTML/CSS не являются тьюринг-полными языками, по этой причине TIOBE не считает их полноценными языками программирования. Шелл-скрипты измеряются отдельно, а VBA вообще не исследуется, насколько я понял. [вернуться]

3. Не все коричневые языки внушают страх: Python, C#, Swift, JavaScript и SQL остаются любимыми. Хотелось бы услышать какие-нибудь теории о причине этого феномена. Кроме того, Scala и Haskell два языка, к которым я питаю слабость единственные зелёные языки в страшном списке. Это просто шум или есть какое-то обоснование??? [вернуться]
Подробнее..

Перевод - recovery mode Scala 3 избавление от implicit. Extension-методы и неявные преобразования

15.01.2021 16:07:39 | Автор: admin


Это моя вторая статья с обзором изменений в Scala 3. Первая статья была про новый бесскобочный синтаксис.


Одна из наиболее известных фич языка Scala имплиситы (от англ. implicit неявный прим. перев.), механизм, который использовался для нескольких разных целей, например: эмуляция extension-методов (обсудим в этой статье), неявная передача параметров при вызове метода, наложение ограничений на возможный тип и др. Все это способы абстрагирования контекста.


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


Scala 3 начинает переход от слишком универсального и слишком широко используемого механизма имплиситов к набору отдельных конструкций для решения конкретных задач. Этот переход растянется на несколько релизов Scala, для того, чтобы разработчикам было проще адаптироваться к новым конструкциям без необходимости сразу переписывать на них весь код. Самой Scala также понадобится переходный период, поскольку в библиотеке коллекций (которая без особых изменений перекочевала из Scala 2.13) имплиситы используются крайне активно.


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


Изменения в имплиситах это обширная тема, которой посвящены две главы в готовящемся 3-ем издании моей книги Programming Scala. Я разобью ее обсуждение здесь на несколько частей, но даже так мы сможем разобрать только главные изменения. Для всей полноты знаний вам придется купить и прочитать мою книгу :) Ну или просто найти интересующие вас детали в документации к Dotty.


Синтаксис примеров актуален на момент Scala 3.0.0-M3.

Extension-методы


Один из способов создания кортежа из двух элементов в Scala использовать a -> b, альтернативу привычному всем (a, b). В Scala 2 это реализовано с помощью неявного преобразования из типа переменной a в ArrowAssoc, где определен метод ->:


implicit final class ArrowAssoc[A](private val self: A) extends AnyVal {  @inline def -> [B](y: B): (A, B) = (self, y)  @deprecated("Use `->` instead...", "2.13.0")  def [B](y: B): (A, B) = ->(y)}

Обратите внимание, что юникодовская стрелочка помечена как deprecated. Не буду объяснять другие детали, типа @inline. (Ну ладно, эта аннотация говорит компилятору пытаться инлайнить этот код, избегая оверхеда на вызов метода...)


Это довольно типично для Scala 2: если хочется чтобы метод казался частью типа, нужно сделать неявное преобразование к типу-обертке, который предоставляет этот метод.


Другими словами, Scala 2 использует универсальный механизм имплиситов, чтобы достичь конкретной цели появления extension-метода. Именно так в других языках (например, в C#) называется способ добавления к типу метода, который объявлен вне этого типа.


В Scala 3 extension-методы становятся сущностями первого класса. Вот как теперь можно переписать ArrowAssoc, используя ~> в качестве имени метода (поскольку настоящий ArrowAssoc все еще существует в Scala 3):


// From https://github.com/deanwampler/programming-scala-book-code-examples/import scala.annotation.targetNameextension [A, B] (a: A)  @targetName("arrow2") def ~>(b: B): (A, B) = (a, b) 

Сначала идет ключевое слово extension, после него типы-параметры (в нашем случае [A, B]). A это тип, который мы расширяем, значение a позволяет сослаться на экземпляр этого типа, для которого был вызван наш extension-метод (аналог this). Обратите внимание, что я использую новый бесскобочный синтаксис, который мы обсуждали в предыдущей статье. После ключевого слова extension можно указать сколько угодно методов. Также можно не писать двоеточие, если метод только один, но я всегда его пишу для единообразия.


Еще одно нововведение в Scala 3 аннотация @targetName. С ее помощью можно определить буквенно-цифровое имя для методов, выполняющих в Scala роль операторов. Это имя нельзя будет использовать из Scala-кода (нельзя написать a.arrow2(b)), зато можно использовать из Java-кода, чтобы вызвать такой метод. Использовать @targetName теперь рекомендуется для всех "операторных" методов.


Неявные преобразования


С появлением extension-методов вам гораздо реже будет нужна возможность конвертации из одного типа в другой, однако иногда такая возможность также может пригодиться. Например, у вас есть финансовое приложение с case-классами для суммы в валюте, процента налогов и зарплаты. Вы хотите для удобства указывать значения этих величин как литерал типа double с последующим неявным преобразованием в типы из предметной области. Вот как это будет выглядеть в интерпретаторе Scala 3:


scala> import scala.language.implicitConversionsscala> case class Dollars(amount: Double):     |   override def toString = f"$$$amount%.2f"     | case class Percentage(amount: Double):     |   override def toString = f"${(amount*100.0)}%.2f%%"      | case class Salary(gross: Dollars, taxes: Percentage):     |   def net: Dollars = Dollars(gross.amount * (1.0 - taxes.amount))// defined case class Dollars// defined case class Percentage// defined case class Salaryscala> given Conversion[Double,Dollars] = d => Dollars(d)def given_Conversion_Double_Dollars: Conversion[Double, Dollars]scala> given d2P: Conversion[Double,Percentage] = d => Percentage(d) def d2P: Conversion[Double, Percentage]scala> val salary = Salary(100_000.0, 0.20)scala> println(s"salary: $salary. Net pay: ${salary.net}")salary: Salary($100000.00,20.00%). Net pay: $80000.00

Сначала мы объявляем, что будем использовать неявные преобразования. Для этого надо импортировать implicitConversions. Затем объявляем три case-класса, которые нужны в нашей предметной области.


Далее показан новый способ объявления неявных преобразований. Ключевое слово given заменяет старое implicit def. Смысл остался тот же, но есть небольшие отличия. Для каждого объявления генерируется специальный метод. Если неявное преобразование анонимное, название этого метода также будет сгенерировано автоматически (обратите внимание на префикс given_Conversion в имени метода для первого преобразования).


Новый абстрактный класс Conversion содержит метод apply, в который компилятор подставит тело анонимной функции, которая идет после =. Если необходимо, метод apply можно переопределить явно:


given Conversion[Double,Dollars] with  def apply(d: Double): Dollars = Dollars(d)

Ключевое слово with знакомо нам по подмешиванию трейтов в Scala 2. Здесь его можно интерпретировать как подмешивание анонимного трейта, который переопределяет реализацию apply в классе Conversion.


Возвращаясь к предыдущему примеру, хотелось бы отметить еще одну новую возможность (на самом деле она появилась еще в 2.13 прим. перев.): можно вставлять подчеркивания _ в длинные числовые литералы для улучшения читаемости. Вы могли такое видеть например в Python (или в Java 7+ прим. перев.).


Scala 3 по-прежнему поддерживает implicit-методы из Scala 2. Например, конвертацию из Double в Dollars можно было бы записать так:


implicit def toDollars(d: Double): Dollars = Dollars(d)

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


Что дальше?


В следующей статье мы рассмотрим новый синтаксис для тайпклассов, который сочетает given и extension-методы. Для затравки, подумайте, как можно было бы добавить метод toJson к типам из нашей предметной области (Dollars и др.), или как реализовать концепции из теории категорий монаду и моноид.

Подробнее..

Перевод Scala 3 избавление от implicit. Тайпклассы

25.01.2021 14:08:49 | Автор: admin


Моя предыдущая статья была про неявные преобразования и extension-методы. В этой статье обсудим новый способ объявления тайпклассов в Scala 3.


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


Но сначала разберемся, что же такое тайпкласс. Как и сама эта концепция, термин "класс типов" (от англ. type class прим. перев.) появился в Haskell. Слово "класс" используется здесь не в том узком смысле, который принят в ООП, а в более широком как обозначение набора сущностей, имеющих что-то общее. ( Я понимаю, что большинство людей, которые будут читать эту статью, имеют ООП-бекграунд, и для них термин "класс типов" звучит примерно как "масло масел", хотя имеется в виду "категория масел". Чтобы избежать путаницы с обычными ООП-классами, я вместо "класса типов" буду использовать просто транслитерацию "тайпкласс" прим. перев.)


Синтаксис примеров актуален на момент Scala 3.0.0-M3.

Тайпкласс определяет абстракцию, которая может быть реализована для множества типов, а затем определяет конкретные реализации для тех конкретных типов, которые нам нужны. Вот пример с использованием нового синтаксиса Scala 3:


// Adapted from this Dotty documentation:// https://dotty.epfl.ch/docs/reference/contextual/type-classes.htmltrait Semigroup[T]:  extension (t: T)    def combine(other: T): T    def <+>(other: T): T = t.combine(other)trait Monoid[T] extends Semigroup[T]:  def unit: T

В математике полугруппа это абстракция сложения, как можно догадаться по введенному нами оператору <+>. Моноид это полугруппа с нейтральным элементом, например, 0 нейтральный элемент для операции сложения. В примере мы объявляем эти абстракции, используя трейты Semigroup и Monoid.


В Semigroup для произвольного типа T добавляются extension-методы combine и <+>, причем combine не реализован. unit в Monoid объявлен как обычный, а не как extension-метод. Это сделано потому, что значение unit для каждого отдельного типа T будет одно, и оно не зависит от того, с каким конкретным значением, имеющим тип T, мы работаем.


Пример реализации моноида для конкретных типов:


given StringMonoid: Monoid[String] with  def unit: String = ""  extension (s: String) def combine(other: String): String = s + othergiven IntMonoid: Monoid[Int] with  def unit: Int = 0  extension (i: Int) def combine(other: Int): Int = i + other

Реализация выглядит достаточно прямолинейно. Стоит лишь отметить, что given foo: Bar это новый синтаксис для implicit-значений. Если ввести код этого примера в Scala3 REPL, можно увидеть, что в реальности создаются два объекта: StringMonoid и IntMonoid.


Давайте теперь попробуем сделать с нашими моноидами что-нибудь полезное:


"2" <+> ("3" <+> "4")             // "234"("2" <+> "3") <+> "4"             // "234"StringMonoid.unit <+> "2"         // "2""2" <+> StringMonoid.unit         // "2"2 <+> (3 <+> 4)                   // 9(2 <+> 3) <+> 4                   // 9IntMonoid.unit <+> 2              // 22 <+> IntMonoid.unit              // 2

StringMonoid и IntMonoid содержат внутри реализацию unit. Оператор <+> объявлен как extension-метод, который вызывается для конкретных экземпляров String или Int. По определению полугруппы <+> должен быть ассоциативным, что и продемонстрировано в примере.


Мы могли бы объявить реализации моноида анонимными: given Monoid[String] with .... Но тогда для доступа к методу unit нам пришлось бы вызывать summon[Monoid[String]]. Где summon это аналог старого implicitly, глобального метода для получения ссылки на implicit-значение из контекста. Или можно использовать автоматически сгенерированное компилятором имя given_Monoid_String, хотя лучше не полагаться на то, что в будущих версиях компилятора будут придерживаться этой же конвенции именования сгенерированных объектов.


Наш пример наглядно демонстрирует, как использовать тайпклассы для реализации общих абстракций для множества целевых типов, включая даже аналог членов объекта-компаньона (в нашем случае это был метод unit). Хотя сгенерированные объекты не являются компаньонами целевых типов.


Наконец, конкретную реализацию тайпкласса можно сделать параметризованной. В примере ниже мы, чтобы не писать реализацию моноида для каждого числового типа, просто обобщаем IntMonoid для любого Numeric[T]:


given NumericMonoid[T](using num: Numeric[T]): Monoid[T] with  def unit: T = num.zero  extension (t: T) def combine(other: T): T = num.plus(t, other)2.2 <+> (3.3 <+> 4.4)             // 9.9(2.2 <+> 3.3) <+> 4.4             // 9.9BigDecimal(3.14) <+> NumericMonoid.unitNumericMonoid[BigDecimal].unit  <+> BigDecimal(3.14)

Обратите внимание на новое ключевое слово using, оно заменяет использовавшееся в Scala 2 implicit для объявления неявных параметров метода. Мы обсудим это подробнее в следующей статье.


Остановимся подробнее на первой строке примера. NumericMonoid это имя реализации тайпкласса, а Monoid[T] ее тип. Поскольку теперь у нас есть параметр T, вместо объекта компилятор сгенерирует класс. И когда мы пишем NumericMonoid[BigDecimal], будет создаваться экземпляр класса NumericMonoid для BigDecimal. num это аргумент конструктора класса NumericMonoid, но благодаря using нам не нужно задавать его явно.


Также обратите внимание на то, как мы вызываем unit. В последней строке мы явно указываем тип-параметр, в то время как в предпоследней он выводится автоматом из типа левого операнда <+>. Вывод типов в Scala не симметричен относительно операции вызова метода obj1.method(obj2).


Что дальше?


В следующей статье мы подробнее рассмотрим новое ключевое слово using и работу со списком неявных параметров метода.

Подробнее..

Еще раз про try и Try

31.01.2021 22:19:51 | Автор: admin

Исключения, проверяемые и нет

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

Например, есть функция по считыванию числа из файла (или не числа, не важно):

String readStoredData(String id) throws FileNotFoundException, IOException {    File file = new File(storage, id + ".dat");    try (BufferedReader in = new BufferedReader(new FileReader(file))) {        return in.readLine();    }}

Как видно, тут нет кода, решающего что делать в случае ошибки. Да и не ясно что делать завершить программу, вернуть "", null или еще что-то? Поэтому исключения объявлены в throws и будут обработаны где-то на вызывающей стороне:

int initCounter(String name) throws IOException, NumberFormatException {    try {        return Integer.parseInt(readStoredData(name));    } catch (FileNotFoundException e) {        return 0;    }}

Исключения в Java делятся на проверяемые (checked) и непроверяемые (unchecked). В данном случае IOException проверяемое вы обязаны объявить его в throws и потом где-то обработать, компилятор это проверит. NumberFormatException же непроверяемое его обработка остается на совести программиста и компилятор вас контролировать не станет.

Есть еще третий тип исключений фатальные ошибки (Error), но их обычно нет смысла обрабатывать, поэтому вас они не должны заботить.

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

С этим подходом есть несколько проблем:

  • функциональное программирование в лице функций высших порядков плохо совместимо с проверяемыми исключениями;

  • непроверяемые исключения обычно теряются и обрабатывать их забывают пока тесты (или того хуже клиенты) не обнаружат ошибку.

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

А что там в Scala?

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

Возьмем к примеру Try[T] это тип, который содержит либо значение, либо исключение. Перепишем наш код на Scala:

def readStoredData(id: String): Try[String] =  Try {    val file = new File(storage, s"$id.dat")    val source = Source.fromFile(file)    try source.getLines().next()    finally source.close()  }def initCounter(name: String): Try[Int] = {  readStoredData(name)    .map(_.toInt)    .recover {      case _: FileNotFoundException => 0    }}

Выглядит вполне похоже, разница в том, что тип результата функции readStoredData уже не String, а Try[String] работая с функцией вы не забудете о возможных исключениях. В этом смысле Try похож на проверяемые исключения в Java компилятор напомнит вам об исключении, но без проблем с лямбдами.

С другой стороны недостатки тоже есть:

  • вы не знаете какие конкретно виды исключений там могут быть (тут можно использовать Either[Error, T], но это тоже не очень удобно);

  • в целом happy-path требует больше синтаксических ритуалов, чем исключения (Try/get или for/map/flatMap);

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

В целом такой подход хорошо расширяется на другие эффекты (в данном случае Try[String] означает строку с эффектом возможностью содержать ошибку вместо значения). Примерами могут быть Option[T] потенциальное отсутствие значения, Future[T] асинхронное вычисление значения и т.п.

Исключения и ошибки

Возвращаясь к исходной проблеме стоит заметить, что если исключения можно избежать это стоит сделать. Собственно именно исходя из этой логики были введены проверяемые/непроверяемые типы исключений в Java, когда непроверяемые исключения говорят об ошибке в коде (а не например в файловой системе).

Поэтому в изначальной реализации функции у нас было два скрытых случая ошибки:

  1. FileNotFoundException если файла нет, что вероятно логическая ошибка или ожидаемое поведение

  2. Другие IOException если файл прочитать не удалось настоящие ошибки среды

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

def readStoredData(id: String): Option[Try[String]] = {  val file = new File(storage, s"$id.dat")  if (file.exists()) Some(    Try {      val source = Source.fromFile(file)      try source.getLines().next()      finally source.close()    }  )  else None}

Тип результата Option[Try[String]] может выглядеть непривычно, но теперь он явно говорит, что результатом могут быть три отдельных случая:

  1. None нет файла

  2. Some(Success(string)) собственно строка из файла

  3. Some(Failure(exception)) ошибка считывания файла, в случае если он существует

Теперь Try содержит только настоящие ошибки среды. В Java в таких случаях часто используются специальные значения, например null. Но если это поведение не выражено в типе его легко пропустить.

Обилие типов создает больше визуального шума и часто требует более сложного кода при работе с несколькими эффектами одновременно. Но за это предоставляет самодокументируемый код и дает возможность компилятору найти многие ошибки.

Подробнее..

DINS Scala School

04.02.2021 14:12:29 | Автор: admin

Приглашаем в онлайн-школу DINS: здесь мы научим программировать на Scala и сделаем оффер лучшим студентам. Прием заявок открыт до 16 февраля. Подробности под катом.


Для кого Scala School

У идеального студента Scala School есть техническое образование и/или опыт программирования. Если технического образования нет не беда: достаточно курсов по программированию или самостоятельного обучения.

На занятия и выполнение домашних заданий потребуется примерно 8 часов в неделю. К моменту старта у студентов должна быть необходимая техника: компьютер на Windows, Mac или Linux, микрофон и камера.

Зачем участвовать

  • Ты научишься писать полноценное бэкенд-приложение на Scala.

  • Получишь сертификат о прохождении обучения.

  • У тебя появится проект для портфолио.

  • Обзаведешься полезными знакомствами в IT-сообществе.

  • Получишь работу в DINS, если хорошо проявишь себя на курсе.

Программа курса

  • Scala 101

  • Коллекции

  • Функциональное программирование

  • Асинхронное программирование

  • Http4s, server & client

  • Базы данных

  • Асинхронное общение, Kafka

  • Подготовка к финальным проектам. Мастер-классы Telegram-боты и Frontend на Scala

  • Фичи и MVP. Protobuf и GRPC

  • Проектная работа, презентация проектов

Как проходят занятия

Курс стартует 2-го марта и продлится до конца мая. Занятия будут проходить с 19:00 до 21:00 МСК по вторникам и пятницам. Все обучение будет в онлайне, так что участвовать можно из любого города.

Как подать заявку

Для поступления в Scala School нужно подать заявку на сайте школы до 16 февраля и сделать тестовое до 21 февраля. Мы внимательно изучим результаты тестовых и сообщим о поступлении до 26 февраля.

Если есть вопросы задавай их в комментариях к посту или пиши на scala-school@dins.ru.

Подробнее..

Перевод Scala 3 Dotty Факты и Мнения. Что мы ожидаем?

04.03.2021 20:15:20 | Автор: admin

Привет, Хабр. Для будущих студентов курса Scala-разработчик подготовили перевод материала.

Приглашаем также на открытый вебинар Эффекты в Scala. Участники вместе с экспертом рассмотрят понятие эффекта и сложности, которые могут возникать при их наличии, а также рассмотрят понятие функционального эффекта и его свойства.


Что такое Scala 3?

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

Что мотивировало появление новой версии, которая связана с самой сутью Scala (а именно DOT-вычисления причина, по которой Scala 3 начиналась как Dotty); в новой версии наблюдается повышение производительности и предсказуемости, что делает код более легким, интересным и безопасным; улучшение инструментария и бинарной совместимости; а также еще более дружелюбное отношение к новичкам.

В этой статье мы выделим некоторые изменения, которые, по нашему мнению, имеют большую ценность для начинающих программистов Scala. Мы также поговорим о процессе миграции и бинарной совместимости. Наконец, в конце мы поделимся нашим мнением об этой новой версии.

Scala 3 новый язык?

Да, это так, потому что в Scala есть много изменений в языке, включая функции, которые будут постепенно выводиться из употребления. А также потому, что процесс обучения отличается от предыдущих версий. Кроме того, там также будет обновляться база знаний.

И одновременно, мы можем ответить нет на данный вопрос выше. Несмотря на изменения, о которых мы расскажем в этой статье, вдобавок ко многим другим, не упомянутым (чтобы статья не была слишком длинной), Scala 3 все еще остается Scala. Основные концепции остаются на месте, а поддержка кросс-билдинга усиливает то, что набор функций имеет большое пересечение с предыдущей версией.

Почему происходит столь много изменений одновременно?

В конце этой статьи вы будете задаваться вопросом, почему там сразу много меняется. Ответ в книге Scala 3. Она представляет собой сам язык. Выпуская все изменения сразу, нет необходимости каждый раз переписывать книгу, так как это делается с многими другими книгами. В основном, изменения затрагивают основы языка, упрощая жизнь его пользователей или заменяя существующие функции. Поэтому предстоящие изменения должны быть ограничены и расставлены по приоритетам в соответствии с основами, упрощениями и ограничениями. Тогда всё, что остаётся изменить в любых возможных более поздних версиях, будет больше связано с добавлением большей функциональности и выразительности, особенно для опытных пользователей, т.е. вещей, которые можно отложить и которые не оказывают резкого влияния на язык.

Scala 3 это новый Python 3?

Существует необоснованное убеждение, что Scala 3 это новый Python 3 относительно его совместимости с предыдущей версией. Однако, есть некоторые аргументы против этого мнения: а именно, что вам не нужно переносить все на Scala 3, так как есть бинарная совместимость с Scala 2.13 (подробнее об этом будет в разделе о миграции); вы можете уверенно мигрировать благодаря сильной системе типа Scala; и есть гораздо больше преимуществ при миграции с 2 на 3 в Scala, чем было бы при миграции с 2 на 3 на Python 3.

Какие изменения ключевые?

Мы выбрали некоторые ключевые функции, которые считаем более актуальными для начинающих программистов Scala. Мы их опишем и прокомментируем то, как они могут на нас повлиять. Мы не будем комментировать все новые возможности, потому что список слишком длинный. В любом случае, эта статья не будет учебным пособием по каждой функции. Если вы хотите увидеть список всех изменений, ссылок и других ресурсов, вы можете посмотреть это на dotty.epfl.ch.

Optional Braces (опциональные или необязательные фигурные скобки)

Одной из самых революционных новинок являются optional braces и использование правил отступа, как в случае с Python. Это действительно революционно, потому что визуально меняется код и это влияет на читабельность достаточно, чтобы невнимательный читатель подумал, что это новый язык. В дополнение к тому, что это приводит к более чистому и короткому коду. Optional braces хорошая вещь, потому что:

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

  2. Даже в случае со скобками почти каждый проект использует некоторые правила для отступов очень строго (это проверяется либо при просмотре кода, либо при принудительном применении scalafmt), так что скобки являются для нас лишь дополнительным токеном для регистрации во время чтения кода, и это не вводит никакой дополнительной информации.)

trait Printer:  def print(msg: String): Unitclass ConsolePrinter extends Printer:  def print(msg: String): Unit = println(msg)class EmojiPrinter(underlying: Printer) extends Printer:  def print(msg: String): Unit =    val emoji = msg match      case ":)"  => ?      case ":D"  => ?      case ":|"  => ?      case other => other    underlying.print(emoji)

Одним из недостатков использования правил отступов является распознавание того момента, когда заканчивается большая область отступов. Для решения этой проблемы Scala 3 предлагает end маркер.

class EmojiPrinter(underlying: Printer) extends Printer:  def print(msg: String): Unit =    if msg != null then      val emoji = msg match        case ":)"  => ?        case ":D"  => ?        case ":|"  => ?        case other => other      underlying.print(emoji)    end ifend EmojiPrinter

Обратите внимание, что мы не ставим скобки, а также обратите внимание на then.

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

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

Не рекомендуется повсеместно ставить end маркер. Подсказка такая использовать его, когда область отступов слишком длинная. Однако, определение слишком длинный может варьироваться от человека к человеку. Таким образом, в соответствии с официальной документацией, end маркер имеет смысл если:

  • Конструктор содержит пустые строки, или

  • Конструктор имеет 15 линий и более

  • Конструктор имеет 4 уровня отступов и более

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

Enums

Почти каждый программист Scala, работающий на Java, пропускает ключевое слово enum и концепцию, которую он воплощает. Представим, что до Scala 3 вам нужно было написать какой-нибудь шаблонный код, чтобы достичь чего-то похожего на перечисление:

sealed trait Colorcase object Red extends Colorcase object Green extends Colorcase object Blue extends Color

В Scala 3, мы можем использовать стандартные типы enum:

enum Color:  case Red, Blue, Green

С годами все больше и больше код пишется с учетом безопасности типа. Такие концепции, как алгебраические типы данных (ADT), стали обычным явлением в системном моделировании. Поэтому было бы целесообразно предложить программистам более простой механизм реализации этих структур данных. Действительно, Scala 3 предлагает более простой способ реализации ADT через enums:

enum Option[+T]:  case Some(x: T) // extends Option[T]       (omitted)  case None       // extends Option[Nothing] (omitted)

Если вы хотите сделать ваш определенный Scala-enum совместимым с Java-enum, вам необходимо расширить java.lang.Enum, который импортируется по умолчанию:

enum Color extends Enum[Color]:  case Red, Blue, Greenprintln(Color.Green.compareTo(Color.Red)) // 2

Если вам интересен более сложный случай перечисления, например, с параметрами или обобщенными ADT, посмотрите на ссылку enums.

Редизайн implicit (неявность)

Несмотря на критику, implicit является одной из наиболее характерных черт Scala. Однако, она также является одной из самых противоречивых. Есть свидетельства о том, что implicit скорее является механизмом, чем реальным намерением, которое заключается в решении проблем. Более того, несмотря на то, что implicit легко сочетается с множеством конструкторов, становится не так легко, когда речь заходит о предотвращении нарушений и неправомерного использования. Поэтому Scala 3 редизайнит особенности implicit, ставя каждый случай использования на своё место. Ниже приведены изменения, которые мы считаем наиболее актуальными в отношении implicit редизайна.

Implicit определения Заданные экземпляры

В данном случае речь идет о том, как Scala 3 использует синтез контекстных параметров для определенного типа. Она заменяет предыдущее implicit использование для этой цели. В Scala 3 вы можете дополнительно указывать имя заданного экземпляра. Если вы пропустите это имя, то компилятор выведет его.

trait Ord[T]:  def compare(a: T, b: T): Intgiven intOrd: Ord[Int] with // with name  def compare(a: Int, b: Int): Int = a - bgiven Order[String] with // without name  def compare(a: String, b: String): Int = a.compareTo(b)

Implicit параметры Использование clauses

Контекстные параметры (или implicit параметры) помогают избежать написания повторяющихся параметров по цепочке вызовов. В Scala 3 вы используете implicit параметры через ключевое слово using. Например, из приведенных выше примеров можно определить функцию min , которая работает с ними.

def min[T](a: T, b: T)(using ord: Ord[T]): T =  if ord.compare(a, b) < 0 then a else bmin(4, 2)min(1, 2)(using intOrd)min("Foo", "Bar")

Когда вам нужно просто переадресовать параметры контекста, вам не нужно их называть.

def printMin[T](a: T, b: T)(using Ord[T]): Unit =  println(min(a, b))

Implicit Импорт Заданный Импорт

Бывают случаи, когда неправильный implicit импорт может стать причиной проблемы Кроме того, некоторые инструменты, такие как IDE и генераторы документации, не справляются с implicit импортом. Scala 3 предоставляет новый способ отличить заданный импорт от обычного.

object A:  class TC  given tc: TC = ???  def f(using TC) = ???object B:  import A._  import A.given  ...

В приведенном выше примере нам пришлось импортировать заданные импорты отдельно, даже после импорта с помощью wildcad (_), потому что в Scala 3 заданные импорты работают не так, как обычные. Вы можете объединить оба импорта в один.

object C:  import A.{using, _}

Вот некоторые спецификации, касающиеся заданных импортов по типам. Посмотрите на данную импортную документацию.

Implicit Conversion Заданная Conversion

Представим, что до Scala 3, если вы хотели определить Implicit Conversion, вам просто нужно было написать Implicit Conversion, которое получает экземпляр исходного типа и возвращает экземпляр целевого типа. Теперь нужно определить данный экземпляр классаscala.Conversion, который ведет себя как функция. Действительно, экземплярыscala. Conversion это функции. Посмотрите на их определение.

abstract class Conversion[-T, +U] extends (T => U):  def apply (x: T): U

Например, здесь можно посмотреть на преобразование от Int к Double и на более короткую версию:

given int2double: Conversion[Int, Double] withdef apply(a: Int): Double = a.toDoublegiven Conversion[Int, Double] = _.toDouble

Основной причиной использования Заданной Conversion является наличие специального механизма преобразования стоимости без каких-либо сомнительных конфликтов с другими конструкторами языка. Согласно данной документации по преобразованию, все другие формы implicit преобразований будут постепенно ликвидированы.

Implicit классы Методы расширения

Методы расширения являются более интуитивным и менее шаблонным способом, чем Implicit классы для добавления методов к уже определенным типам.

case class Image(width: Int, height: Int, data: Array[Byte])extension (img: Image)  def isSquare: Boolean = img.width == img.heightval image = Image(256, 256, readBytes("image.png"))println(image.isSquare) // true

Методы расширения могут иметь параметры типа как в их определении, так и в их методах. Их определения также могут иметь несколько методов.

extension [T](list: List[T])def second: T = list.tail.headdef heads: (T, T) = (list.head, second)

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

Типы пересечения и соединения

Scala 3 предоставляет новые способы объединения типов, два из которых Типы пересечения и соединения.

Типы пересечения

Типы пересечения это типы, элементы которых принадлежат к обоим типам, составляющим его. Они определяются оператором & более двух типов. & являются коммутаторами: A & B производит один и тот же тип B & A. Они также могут быть сцеплены, так как они тоже являются типами.

trait Printable[T]: def print(x: T): Unittrait Cleanable: def clean(): Unittrait Flushable: def flush(): Unitdef f(x: Printable[String] & Cleanable & Flushable) = x.print("working on...") x.flush() x.clean()

Вам может быть интересно узнать, как компилятор решает конфликты разделяемых элементов. Ответ состоит в том, что компилятор в этом не нуждается. Типы пересечений представляют собой требования к members типам (члены типов). Они работают практически так же, как и при формировании типов. В момент построения members необходимо просто убедиться, что все полученные members определены корректно.

trait A:  def parent: Option[A]trait B:  def parent: Option[B]class C extends A,B:  def parent: Option[A & B] = None  // or  // def parent: Option[A] & Option[B] = Nildef work(x: A & B) =  val parent:[A & B] = x.parent  // or  // val parent: Option[A] & Option[B] = x.parent  println(parent) // Nonework(new C)

Заметьте, что в in class C нам нужно решить конфликты связанные с тем, что children member появляется и в A и в B. То есть тип C это пересечение его типа в A и его типа в B, например, Option[A] & Option[B] могут быть упрощены в вид Option[A & B], так как Option (опция) является ковариантной.

Типы соединения

Тип соединения A | B принимает все экземпляры типа A и все экземпляры типа B. Обратите внимание, что мы говорим об экземплярах, а не о members (членах), как это делают типы пересечения. Поэтому, если мы хотим получить доступ к его members, нам нужно сопоставить их по шаблону.

def parseFloat(value: String | Int): Float =   value match     case str: String => str.toFloat    case int: Int => int.floatValueparseFloat("3.14") // 3.14parseFloat(42) // 42.0

Типы соединения не выводятся автоматически. Если вы хотите, чтобы тип определения (val, var или def) был типом соединения, вам нужно сделать это явно, иначе компилятор выведет наименьший общий ancestor (предок).

val any = if (cond) 42 else "3.14" // Anyval union: String | Int = if (cond) 42 else "3.14" // String | Int

Почётные упоминания

Некоторые другие изменения довольно актуальны и также заслуживают здесь упоминания.

Трейт параметры

Scala 3 позволяет трейтам иметь параметры. Эти параметры оцениваются непосредственно перед инициализацией трейта. Параметры трейта являются заменой для ранних инициализаторов, которые были удалены из Scala 3.

Универсальные применяемые методы

Конструкторы Case class стали достаточно популярными, и многие разработчики пишут Case class просто для того, чтобы не писать new для создания объектов. Поэтому в Scala 3 больше не нужно писать new для создания экземпляров классов.

Типы Opaque

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

Export clauses

Export clauses это простой способ передачи members (членов) от одного типа к другому без какого-либо наследования. Откладывая export от членов-класса (включая трейты и объекты) в тело другого класса (также включая трейты и объекты), вы копируете members и делаете их доступными через экземпляры целевых классов.

Редизайн метапрограммирования

В Scala 2 макросы оставались экспериментальной функцией. В связи с тем, что макросы сильно зависят от компилятора Scala 2, поэтому мигрировать на Scala 3 оказалось невозможным. В метапрограммировании Scala 3 появились новые конструкции, облегчающие его использование. Взгляните на обзор метапрограммирования Scala 3.

Ограничения и удаленные фитчи

Чтобы упростить язык и сделать его более безопасным, Scala 3 ограничивает количество опций и снимает некоторые функции с производства. Самые важные из них:

  • Ограничение проекций типов (C#P) только в отношении классов, т.е. абстрактные типы больше не поддерживают их;

  • Для использования надстрочной нотации, модификатор infix должен быть помечен на желаемых методах;

  • Мультиверсальное равенство - это оптический способ избегания неожиданных равнозначностей;

  • Implicit преобразования и приведенные выше импорты также являются видами ограничений;

  • Специальная обработка трейта DelayedInit больше не поддерживается;

  • Отброшен синтаксис процедуры (опускание типа возврата и = при определении функции);

  • Библиотеки XML все еще поддерживаются, но будут удалены в ближайшем будущем;

  • Автоматическое приложение, когда пустой список аргументов () неявно вставляется при вызове метода без аргументов и больше не поддерживается;

  • Символьные литералы больше не поддерживаются.

Полный список выпавших функций доступен в официальной документации.

Нужно ли вам переходить на Scala 3?

Прежде всего, есть очень хорошо сделанная документация, посвященная исключительно переходу на Scala 3. Здесь мы просто поделимся некоторыми мыслями, которые вы можете учесть, когда начнете использовать Scala 3 в своих текущих проектах.

Хорошо известно, что рекомендуется быть в курсе работы со стеком технологий и зависимостями, так как они могут исправлять ошибки и улучшать удобство использования, производительность и так далее. Это также относится и к Scala. Независимо от того, насколько мало усилий прилагается для перехода, иногда необходимо согласовать это с заинтересованными сторонами. Тем не менее, миграция Scala 3 была спроектирована так, чтобы быть как можно более плавной. Это означает, что вы можете воспользоваться самыми заметными эволюциями в языке и сделать программирование более легким, интересным и безопасным.

Какое время подходит для перехода на Scala 3?

Мы хотели бы порекомендовать вам перейти на Scala 3 прямо сейчас, но мы знаем, что есть факторы, выходящие за рамки возможностей даже большого энтузиазма. Если следующее звучит как отличный аргумент, чтобы убедить вашего руководителя, то не забывайте, что Scala 3 сохраняет как обратную, так и прямую совместимость с Scala 2.13 (за исключением макросов), не всю совместимость, но при каждой несовместимости есть решение для кросс-компиляции.

Что такое бинарная совместимость в Scala 3?

Scala 3 предлагает обратную двоичную совместимость с Scala 2. Это означает, что вы все еще можете полагаться на библиотеку Scala 2. Начиная с версии Scala 2.13.4, выпущенной в ноябре 2020 года, вы можете использовать библиотеки, написанные на Scala 3. Таким образом, в Scala 3 вы получаете двойную совместимость туда и обратно.

Scala 3 поддерживает обратную и прямую совместимость с помощью уникального революционного механизма в экосистеме Scala. Scala 3 выводит файлы TASTy и поддерживает Pickle из версий Scala 2.x. Scala 2.13.4 поставляется с считывателями TASTy, поэтому поддерживает как все традиционные функции, так и новые, такие как Enums, Intersection types (типы соединения) и другие. Дополнительные сведения см. в руководстве по совместимости.

Заключение

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

В целом, мне кажется, что Scala 3 это отличная доработка Scala 2. Мы научились жить без многих вещей: для одних это были библиотеки, решавшие проблемы в той или иной степени с ограничениями, для других это было либо невозможно, либо выходило за пределы их зоны комфорта. Поэтому, как только Scala 3 получит широкое распространение, мы ожидаем увидеть более грамотно написанный код, в основном потому, что там это намного проще.


Узнать подробнее о курсе Scala-разработчик.

Смотреть открытый вебинар Эффекты в Scala.

Подробнее..

Перевод Основы Cat Concurrency с Ref и Deferred

10.03.2021 18:12:48 | Автор: admin

Параллельный доступ и ссылочная прозрачность

Для будущих учащихся на курсе Scala-разработчик приготовили перевод материала.

Приглашаем также на вебинар по теме
Эффекты в Scala. На занятии рассмотрим понятие эффекта и сложности, которые могут возникать при их наличии. Также введем понятие функционального эффекта, рассмотрим его свойства и реализуем свой небольшой функциональный эффект. Присоединяйтесь.


*Concurrency конкурентность, допускающая одновременное выполнение нескольких вычислительных процессов.

Ref и Deferred являются основными строительными блоками в FP, используемыми параллельно, в манере concurrent. Особенно при использовании c tagless final (неразмеченной конечной) абстракцией, эти два блока, при построении бизнес-логики, могут дать нам и то, и другое: параллельный доступ (concurrent access) и ссылочную прозрачность (referential transparency), и мы можем использовать их для построения более продвинутых структур, таких как counters (счетчики) и state machines (конечные автоматы).

Перед тем, как мы углубимся в Ref и Deferred, нам полезно узнать, что concurrency в Cats строится на Java AtomicReference, и здесь мы и начнем наше путешествие.

Atomic Reference

AtomicReference это один из элементов пакета java.util.concurrent.atomic. В Oracle docs мы можем прочитать, чтоjava.util.concurrent.atomic это:

Небольшой инструментарий классов, поддерживающих потокобезопасное программирование без блоков с одиночными переменными. По сути, классы в данном пакете расширяют понятие volatile значений, полей и элементов массива до тех, которые также обеспечивают условную операцию atomic обновления

Экземпляры классов AtomicBoolean, AtomicInteger, AtomicLong, и AtomicReference обеспечивают доступ и обновление от одиночных переменных к соответствующему типу (функционального блока).

AtomicReference с нами начиная с Java 1.5 и используется для получения лучшей производительности, чем синхронизации (хотя это не всегда так).

Когда вам приходится совместно использовать некоторые данные между нитями (threads), вы должны защитить доступ к этой части данных. Самым простым примером будет увеличение некоторого количества int: i = i + 1. Наш пример состоит из фактически 3 операций, сначала мы читаем значение i , затем добавляем 1 к этому значению, а в конце снова присваиваем вычисленное значение i . В отношении многопоточных приложений, мы можем столкнуться с ситуацией, когда каждый thread будет выполнять эти 3 шага между шагами другого thread, а конечное значение i предсказать не удастся.

Обычно в вашей голове появляется слово synchronised или механизм класса lock, но с atomic.* вам больше не нужно беспокоиться о явной синхронизации, и вы можете перейти на предоставленные atomic (атомарные) типы утилит, где проверка выполнения операции в один шаг включается автоматически.

Давайте, возьмем для примера AtomicInteger.incrementAndGet:

/**     * Atomically increments by one the current value.     *     * @return the updated value     */    public final int incrementAndGet() {        for (;;) {            int current = get();            int next = current + 1;            if (compareAndSet(current, next))                return next;        }    }

С помощью операции compareAndSet мы либо обновляем наши данные, либо терпим неудачу, но никогда не заставляем thread ждать. Таким образом, если операция compareAndSet в incrementAndGet не удаётся, мы просто пытаемся повторить всю операцию заново, извлекая текущее значение наших данных с помощью функции get() в начале. С другой стороны, при использовании синхронизированных механизмов нет ограничений на количество операторов (statement), которые вы хотите выполнить во время блокировки, но этот блок никогда не выйдет из строя и может заставить вызывающий thread ждать, предоставляя возможность заблокировать или снизить производительность.

Теперь, зная определенные основы, давайте перейдем к нашей первой мега-звезде concurrency.

Ref

Ref в Cats очень похож на упомянутую выше atomic (атомарную) ссылку Java. Основные отличия заключаются в том, что Ref используется с tagless final абстракцией F . Он всегда содержит значение, а значение, содержащееся в Ref типа A, всегда является неизменным (immutable).

abstract class Ref[F[_], A] {  def get: F[A]  def set(a: A): F[Unit]  def modify[B](f: A => (A, B)): F[B]  // ... and more}

Ref[F[_], A] это функциональная изменяемая (mutable) ссылка:

  • Concurrent ( конкурентная)

  • Lock free ( без блоков)

  • Всегда содержит значение

Она создается путем предоставления начального значения, и каждая операция осуществляется в
F, например, cats.effect.IO.

Если мы внимательно посмотрим на сопутствующий объект для Cats Ref, мы увидим, что наша F должна соответствовать некому требованию, а именно быть Sync.

def of[F[_], A](a: A)(implicit F: Sync[F]): F[Ref[F, A]] = F.delay(unsafe(a))

Вышеприведенный метод является лишь примером многих операций, доступных на нашем Ref; он используется для построения Ref с исходным значением.

Sync дает нам возможность приостанавливать любые побочные эффекты с помощью метода
delayдля каждой операции на Ref.

Ref довольно простая конструкция, мы можем сосредоточиться в основном на ее get, set и of чтобы понять, как она работает.

Метод get and set

Допустим, у нас есть объект (для этого блога мы назовем его Shared), который нужно обновить несколькими threads, и мы используем для этого наши методы get и set , создавая утилитный метод, который поможет нам в дальнейшем:

def modifyShared(trace: Ref[IO, Shared], msg: String): IO[Unit] = {for {sh <- trace.get()_ <- trace.set(Shared(sh, msg))} yield ()}

Наш Shared объект может быть построен путем использования его предыдущего состояния и нового значения для создания нового экземпляра Shared, который может быть на самом деле всем, что мы хотим простым списком, картой или чем угодно, к чему мы хотим получить одновременный безопасный доступ.

Я только что создал Shared(prev: Shared, msg: String) для данной статьи.

В нашем примере выше F был заменён конкретным IO из Cats Effect, но имейте в виду, что Ref является полиморфным в F и может быть использован с другими библиотеками.

С помощьюmonadic (монадический) IO мы применяем функцию flatMap на каждом шаге и устанавливаем значение, сохраненное в нашем Ref на желаемое значение или... подождите, может быть, мы этого не делаем.

При таком подходе, когда modifyShared будет вызываться одновременно, и мы можем потерять обновления! Это происходит потому, что мы можем столкнуться с ситуацией, когда, например, двое threads могут прочитать значение с помощью get и каждый из них будет выполнять set одновременно. Методы get и set не вызываются атомарно (atomically) вместе.

Atomic (атомарный) update

Конечно, мы можем улучшить приведенный выше пример и использовать другие доступные методы из Ref. Для совместной реализации get и set мы можем использовать update.

def update(f: A => A): F[Unit] 

Это решит нашу проблему с обновлением значения, однако update имеет свои недостатки. Если мы захотим обратиться к переменной сразу после обновления, аналогично тому, как мы использовали get и set , мы можем в итоге получить устаревшие данные, допустим, наш Ref будет содержать ссылку на Int:

for {_ <- someRef.update(_ + 1)curr <- someRef.get_ <- IO { println(s"current value is $curr")}} yield ()

Нас спасет modify

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

def modify[B](f: A => (A, B)): F[B] = {      @tailrec      def spin: B = {        val c = ar.get        val (u, b) = f(c)        if (!ar.compareAndSet(c, u)) spin        else b      }      F.delay(spin)    }

Как видите, это практически та же имплементация, что и в примере с AtomicInteger.incrementAndGet, который я показывал в начале, но только в Scala. Нам четко видно, что для выполнения своей работы Ref также работает на основе AtomicReference .

Ref ограничения

Вы, вероятно, уже заметили, что в случае неудачи при обновлении значения функция, переданная update/ modify, должна быть запущена недетерминированно (nondeterministically) и, возможно, должна быть запущена несколько раз. Хорошая новость заключается в том, что это решение в целом оказывается намного быстрее, чем стандартный механизм блокировки и синхронизации, и гораздо безопаснее, так как это решение не может быть заблокировано.

Как только мы узнаем, как работает простой Ref, мы можем перейти к другому классу Cats Concurrent: Deferred (Отложенный вызов).

Deferred

В отличие от Ref, Deferred:

  • создается пустым (отложенный результат выполнения)

  • может быть выполнен один раз

  • и после установки его нельзя изменить или снова сделать пустым.

Эти свойства делают Deferred простым и в то же время довольно интересным.

abstract class Deferred[F[_], A] {  def get: F[A]  def complete(a: A): F[Unit]}

Deferred используется для явной функциональной синхронизации. Когда мы вызываем get в пустой Deferred мы устанавливаем блокировку до того момента, как значение станет вновь доступно. В соответствии с документацией из самого класса:

  • Блокировка указана только семантическая, никакие реальные threads (нити) не блокируются имплементацией

Тот же вызов get непустого Deferred немедленно вернет сохраненное значение.

Другой метод complete заполнит значение, если экземпляр пуст и при вызове непустого Deferred приведет к сбою (неудачная попытка IO).

Здесь важно отметить, что Deferred требует, чтобы F было Concurrent, что означает, что его можно отменить.

Хорошим примером использования Deferred является ситуация, когда одна часть вашего приложения должна ждать другую.

Пример ниже взят из великолепного выступления Фабио Лабеллы на выставке Scala Italy 2019 Composable Concurrency with Ref + Deferred available at Vimeo

def consumer(done: Deferred[IO, Unit]) = for {c <- Consumer.setup_ <- done.complete(())msg <- c.read_ <- IO(println(s"Received $msg"))} yield ()def producer(done: Deferred[IO, Unit]) = for {p <- Producer.setup()_ <- done.getmsg = "Msg A"_ <- p.write(msg)_ <- IO(println(s"Sent $msg"))} yield ()def prog = for {  d <- Deferred[IO, Unit]  _ <- consumer(d).start  _ <- producer(d).start} yield ()

В приведенном выше примере у нас есть producer (производитель) и consumer (потребитель), и мы хотим, чтобы producer ждал, пока consumer setup закончится, прежде чем писать сообщения, в противном случае все, что бы мы ни написали в producer, будет потеряно. Для преодоления этой проблемы мы можем использовать общий экземпляр Deferred и блокировать get до тех пор, пока не будет заполнен экземпляр done Deferred со стороны consumer (значение в данном случае простая Unit () ).

Конечно, вышеуказанное решение не обошлось без проблем, когда consumer setup никогда не прекращался, мы застревали в ожидании, а producer не мог отправлять сообщения. Чтобы преодолеть это, мы можем использовать таймаут с get , а также использовать Either[Throwable, Unit] или какую-либо другую конструкцию вместо простой Unit внутри нашего объекта Deferred.

Deferred довольно прост, но в сочетании с Ref он может быть использован для построения более сложных структур данных, таких как semaphores (семафоры).

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


Узнать подробнее о курсе Scala-разработчик.

Смотреть открытый вебинар по теме Эффекты в Scala.

Подробнее..

Неявный вывод в Scala

29.03.2021 22:09:33 | Автор: admin

Многие начинающие и не очень Scala разработчики принимают implicits как умеренно полезную возможность. Использование обычно ограничивается передачей ExecutionContextво Future. Другие же избегают неявного и считают возможность вредной.

Код же вроде этого вообще многих пугает:

implicit def function(implicit argument: A): B

Но я считаю этот механизм важным преимуществом языка, давайте разберемся почему.

Кратко про implicits

В целом implicits это механизм автоматического дополнения кода при компиляции:

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

  • для неявных преобразований значение автоматически оборачивается в вызов метода

Я не буду углубляться в эту тему, кому интересно посмотрите об этом видео от создателя языка. Вкратце один этот механизм используется в нескольких разных случаях (и в Scala 3 будет разделен на несколько отдельных возможностей):

  1. Передача неявного контекста (как ExecutionContext)

  2. Неявные преобразования (не рекомендуется к использованию)

  3. Методы-расширения (extension methods, синтаксический сахар по добавлению методов к существующим типам)

  4. Классы типов (type classes) и неявный вывод (implicit resolution)

Классы типов и неявный вывод

Сами по себе классы тыпов не несут какой-то новизны или революции это попросту реализация методов "для" типа, а не "в самом" типе, это как разница между Comparable& Ordering(Comparatorв Java):

Comparable реализуется для добавления возможности сравнения в ООП стиле:

class Person(age: Int) extends Comparable[Person] {  override def compareTo(o: Person): Int = age compareTo o.age}def max[T <: Comparable[T]](xs: Iterable[T]): T = xs.reduce[T] {  case (a, b) if (a compareTo b) < 0 => b  case (a, _) => a}

Orderingреализуется с той же целью, но отдельно от самого типа:

case class Person(age: Int)implicit object ByAgeOrdering extends Ordering[Person] {  override def compare(o1: Person, o2: Person): Int = o1.age compareTo o2.age}def max[T: Ordering](xs: Iterable[T]): T = xs.reduce[T] {  case (a, b) if Ordering[T].lt(a, b) => b  case (a, _) => a}// is syntactic sugar fordef max[T](xs: Iterable[T])(implicit evidence: Ordering[T]): T = ...

Вот и весь класс типов.

Интересным этот механизм делает неявный вывод. Вернемся к непонятному объявлению из начала статьи и добавим к нему разные комбинации:

implicit val value: A = ???implicit def definition: B = ???implicit def conversion(argument: C): D = ???implicit def function(implicit argument: E): F = ???

В чем разница? Исключая conversion, который является неявным преобразованием, почти ни в чем: value, definition& functionмогут быть использованы чтобы подставить значение для неявного аргумента нужного типа. Разница только в способе вычисления: valстатичен, а defвычисляется каждый раз при постановке. Неявный аргумент в такой функции работает как обычно требует неявного значения в скоупе.

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

Рассмотрим пример мы можем описать порядок пар любых типов, для этого не нужно знать их заранее достаточно знать, что у них тоже есть порядок:

implicit def pairOrder[A: Ordering, B: Ordering]: Ordering[(A, B)] = {  case ((a1, b1), (a2, b2)) if Ordering[A].equiv(a1, a2) => Ordering[B].compare(b1, b2)  case ((a1,  _), (a2,  _)) => Ordering[A].compare(a1, a2)}// again, just syntactic sugar for:implicit def pairOrder[A, B](implicit a: Ordering[A], b: Ordering[B]): Ordering[(A, B)] = ...

Теперь если вдруг нам понадобится найти максимум среди сложных структур компилятор сам построит подходящий объект, комбинируя пользовательские и стандартные объявления:

val values = Seq(  (Person(30), ("A", "A")),  (Person(30), ("A", "B")),  (Person(20), ("A", "C")))max(values) // => (Person(30),(A,B))

У нас был список типа Seq[(Person, (String, String))]и компилятор смог сам подобрать комбинацию функций для построения Orderingдля этого типа:

max(values)(  pairOrder(    ByAgeOrdering,     pairOrder(Ordering.String, Ordering.String)  ))

Так неявный вывод позволяет описывать общие правила вывода и поручить компилятору свести эти правила вместе и получить конкретную имплементацию класса типов. Добавляя собственный тип или собственные правила не нужно описывать все с начала компилятор скомбинирует все сам, чтобы получить нужный объект.

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

Надеюсь неявное стало теперь немного более явным.

Подробнее..

Перевод SCALA 3

16.05.2021 00:04:13 | Автор: admin


После 8 лет работы 28 000 коммитов, 7 400 пул реквестов, 4 100 закрытых issues Scala 3 наконец-то вышла. С момента первого коммита 6 декабря 2012 года более ста человек внесли свой вклад в проект. Сегодня Scala 3 включает в себя последние исследования в области теории типов, а также отраслевой опыт Scala 2. Мы увидели, что хорошо (или не очень хорошо) работает для сообщества в Scala 2. На основе этого опыта мы создали третью итерацию Scala простую в использовании, изучении и масштабировании.

Новые интересные функции: с чего начать?


В Scala 3 появилось множество новых функций, и вы можете задаться вопросом, с чего начать все это изучать. Лучшее место для изучения Scala 3 это его документация.

Чтобы ощутить вкус всех новых функций, которые вошли в Scala 3, вы можете прочитать статью Новое в Scala 3. Для упрощенного и более подробного введения смотрите Scala 3 Book. Вы можете попробовать Scala 3 онлайн, не устанавливая ничего на свой компьютер, через Scastie, или вы можете следовать руководству по началу работы, чтобы установить его на свой компьютер.

Одна вещь полностью изменилась в Scala 3 по сравнению со Scala 2: это макросы. Вы можете узнать больше о том, как они работают в Scala 3, в специальной документации.

Если вы опытный пользователь Scala 2, скорее всего, у вас есть проекты, которые вы хотите перенести со Scala 2 на Scala 3. Вам пригодится руководство по миграции. В нем описывается история совместимости между Scala 2 и Scala 3: исходная совместимость, двоичная совместимость, измененные и удаленные функции, метапрограммирование.

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

Экосистема


Кто уже пользуется Scala 3? Какие библиотеки вы уже можете использовать? Отличное место для ответа на этот вопрос Scaladex. Scaladex это индекс библиотек Scala, в котором вы можете исследовать экосистему по языковой версии, платформе или типу работы, которую выполняет библиотека. На момент написания этой статьи в Scala 3 насчитывалось 308 early adopter библиотек по сравнению с 2597 библиотеками Scala 2.13.

Выпуски и гарантии в эпоху 3.x


Мы намерены продолжать выпускать обновления каждые 6 недель после 3.0.0, каждый раз повышая версию патча. Стабильному выпуску 3.0.x будет предшествовать релиз-кандидат 3.0.x-RC1 за 6 недель до стабильного выпуска. Такие выпуски патчей будут содержать исправления ошибок, влияющих на соответствующую дополнительную версию. Версии патчей будут иметь прямую и обратную совместимость друг с другом в отношении совместимости с исходным кодом, двоичной и TASTy совместимости.

Конечно, мы намерены продолжить развитие языка не только исправлением ошибок, но и другими способами. Новые языковые функции и стандартные библиотечные API появятся в следующих дополнительных версиях. Как и любое добавление в библиотечные API, они могут нарушить обратную совместимость исходного кода незначительными, редкими способами. Однако второстепенные выпуски не нарушат обратную двоичную или TASTy совместимость. Конкретно это означает, что библиотеки, созданные с помощью Scala 3.0.0, будут продолжать работать со Scala 3.x.y!

Авторы библиотеки: Присоединяйтесь к нашему сообществу


В Scala 3 теперь есть набор широко используемых общественных библиотек, которые создаются для каждого ночного снимка Scala 3. На данный момент в нем 67 проектов, и их количество стремительно растет. Присоединяйтесь к нашей сборке сообщества, чтобы убедиться, что наш пакет регрессии включает вашу библиотеку.

Контрибьюторы


Спасибо всем участникам, которые сделали возможным этот релиз

Согласно git shortlog -sn --no-merges 2308509d2651ee78e1122b5d61b798c984c96c4d..3.0.0, постоянные участники, которые сделали Scala 3 реальностью, являются:
список
8661 Martin Odersky
3186 Nicolas Stucki
1435 Guillaume Martres
976 Dmitry Petrashko
797 Liu Fengyun
774 Felix Mulder
407 Allan Renucci
324 liu fengyun
324 Olivier Blanvillain
323 Martin Duhem
224 Aleksander Boruch-Gruszecki
204 Jamie Thompson
201 Krzysztof Romanowski
200 Sbastien Doeraene
172 Paolo G. Giarrusso
171 Bryan Abate
163 Aggelos Biboudis
162 Anatolii Kmetiuk
160 Anatolii
129 Robert Stoll
103 bishabosha
103 Filip Zybaa
101 Miles Sabin
82 Antoine Brunner
64 poechsel
64 Guillaume Raffin
62 Tom Grigg
61 Lan, Jian
55 noti0na1
54 Andrzej Ratajczak
51 odersky
50 Nikita Eshkeev
44 Guillaume R
37 Stphane Micheloud
34 Enno Runne
33 Sara Alemanno
31 Pawe Marks
30 Ondrej Lhotak
29 Som Snytt
29 Abel Nieto
26 Ruslan Shevchenko
25 VladimirNik
23 Adrien Piquerez
22 Raphael Jolly
22 Jonathan Brachthauser
22 Micha Paka
20 vsalvis
20 Tobias Bordenca
20 Fengyun Liu
19 Martijn Hoekstra
19 Samuel Gruetter
19 Phil
19 Maxime Kjaer
18 Jendrik Wenke
17 Jason Zaugg
16 Krzysztof Romanwoski
16 Arnaud ESTEVE
15 Dale Wijnand
14 Jaemin Hong
13 gzoller
13 Vlad Ureche
12 Miron Aseev
12 Wojtek Swiderski
11 Yichen Xu
11 Grzegorz Bielski
10 Sebastian Nadorp
10 Jentsch
10 bjornregnell
10 Arnaud Esteve
10 Dmytro Melnychenko
10 Lionel Parreaux
9 Jonathan Brachthuser
9 yu-croco
9 Jasper Moeys
8 Clemens Winter
8 Lukas Rytz
8 Varunram Ganesh
8 Oron Port
8 Reto Habltzel
7 lafur Pll Geirsson
7 Varunram
7 benkobalog
7 Eugene Melekhov
6 jvican
6 Seth Tisue
6 Natsu Kagami
6 Thierry Treyer
6 Akhtiam Sakaev
6 Olivier ROLAND
6 Olafur Pall Geirsson
5 Nada Amin
5 Michael Pilquist
5 Ausmarton Zarino Fernandes
5 k0ala
5 Vlastimil Dort
5 Valthor Halldorsson
5 Travis Brown
5 Tomasz Godzik
5 Alex Merritt
5 Guillaume Mass
5 Alexander Myltsev
5 Saloni Vithalani
5 Raphael Bosshard
5 Julien Richard-Foy
4 Micha Gutowski
4 Sebastian Harko
4 fhackett
4 ysthakur
4 Ben Elliott
4 Raymond Tay
4 Ayush
4 Neeraj Jaiswal
4 Sarunas Valaskevicius
4 Lucas Burson
4 Dotty CI
4 Eric K Richardson
4 Vitor Vieira
4 Yevgen Nerush
4 Shane Delmore
4 Andrew Valencik
4 senia-psm
4 Minghao Liu
4 Matt D'Souza
4 Eugene Yokota
4 Hanns Holger Rutz
4 Alex Zolotko
4 Georg Schmid
4 Chris Birchall
4 december32
4 Ingar Abrahamsen
3 Michal Gutowski
3 Gabriele Petronella
3 Gabi Volpe
3 Master-Killer
3 Uko
3 Timothe Floure
3 xuwei-k
3 Eric Loots
3 Enno
3 Edmund Noble
3 Saurabh Rawat
3 Albert Chen
3 Jakob Odersky
3 Daniel Li
3 Dani Rey
3 ansvonwa
3 duanebester
3 Alexandre Archambault
3 jerylee
3 kenji yoshida
3 Artur Opala
3 Adriaan Moors
3 Ankit Soni
3 Adam Fraser
3 Pavel Shirshov
3 Joo Pedro Evangelista
3 Andrea Mocci
3 Krzysztof Bochenek
3 Tudor Voicu
2 Tobias Schlatter
2 Alden Torres
2 AnEmortalKid
2 Andrew Zurn
2 Ara Adkins
2 Artsiom Miklushou
2 Ashwin Bhaskar
2 Aurlien Richez
2 Camila Andrea Gonzalez Williamson
2 Dvir Faivel
2 Fabian Page
2 FabioPinheiro
2 Francois GORET
2 Glavo
2 Greg Pevnev
2 Henrik Huttunen
2 Hermes Espnola Gonzlez
2 James Thompson
2 Jan Christopher Vogt
2 Jens Kat
2 Jim Van Horn
2 Jon Pretty
2 Lorand Szakacs
2 Luc Henninger
2 Lucas
2 Matthew Pickering
2 Matthias Sperl
2 Mikael Blomstrand
2 Nadezhda Balashova
2 Nikolay
2 Nikolay.Tropin
2 Patrik Mada
2 Philippus
2 Philippus Baalman
2 Radosaw Wako
2 Rafal Piotrowski
2 Robert Soeldner
2 Roberto Bonvallet
2 Rodrigo Fernandes
2 Steven Heidel
2 Thiago Pereira
2 Tudor
2 William Narmontas
2 changvvb
2 dos65
2 esarbe
2 johnregan
2 lloydmeta
2 typeness
2 veera venky
2 xhudik
2 ybasket
1 Jyotman Singh
1 Justin du Coeur, AKA Mark Waks
1 Julien Jean Paul Sirocchi
1 Joo Pedro de Carvalho
1 rsoeldner
1 Jonathan Skowera
1 Jonathan Rodriguez
1 Jon-Anders Teigen
1 ruben
1 Alexander Slesarenko
1 Pierre Ricadat
1 Piotr Gabara
1 squid314
1 tOverney
1 Raj Parekh
1 Rajesh Veeranki
1 John Sullivan
1 Johannes Rudolph
1 Joan
1 Jimin Hsieh
1 Richard Beddington
1 Rick M
1 Rike-Benjamin Schuppner
1 tanaka takaya
1 Jean Detoeuf
1 tanishiking
1 tim-zh
1 Jarrod Janssen
1 Jan Rock
1 Sam Desborough
1 Jakub Kozowski
1 Sandro Stucki
1 Jacob J
1 Jaap van der Plas
1 Ivano Pagano
1 Ivan Youroff
1 Iltotore
1 Serhii Pererva
1 Igor Mielientiev
1 Ignasi Marimon-Clos
1 Simon Hafner
1 Simon Popugaev
1 Ian Tabolt
1 SrTobi
1 Stefan Zeiger
1 Stephane MICHELOUD
1 tokkiyaa
1 Stphane MICHELOUD
1 Herdy Handoko
1 Szymon Pajzert
1 Harrison Houghton
1 Taisuke Oe
1 yytyd
1 Harpreet Singh
1 Haemin Yoo
1 Timur Abishev
1 Grzegorz Kossakowski
1 Tobias Kahlert
1 0xflotus
1 Greg Zoller
1 Tomas
1 George Leontiev
1 Florian Schmaus
1 zgrybus
1 Florian Cassayre
1 Ferhat Aydn
1 Umayah Abdennabi
1 Fedor Shiriaev
1 Dmitry Melnichenko
1 Dmitrii Naumenko
1 Vasil Vasilev
1 Victor
1 Deon Taljaard
1 Denis Buzdalov
1 Dean Wampler
1 David Hoepelman
1 Vykintas Narmontas (William)
1 Alexander Shamukov
1 DarkDimius
1 Daniel Reigada
1 Daniel Murray
1 Yilin Wei
1 Zoltn Elek
1 adpi2
1 aesteve
1 amanjpro
1 andreaTP
1 Damian Albrun
1 ayush
1 benkbalog
1 Csongor Kiss
1 Ciara O'Brien
1 Carlos Quiroz
1 brunnerant
1 =
1 costa100
1 Bunyod
1 dieutth
1 AlexSikia
1 Brian Wignall
1
1 felher
1 Brandon Elam Barker
1 fschueler
1 gan74
1 gnp
1 gosubpl
1 Bojan Dunaj
1 iroha168
1 Ben Hutchison
1 Albert Serrall Ros
1 Batanick
1 Bartosz Krasiski
1 August Nagro
1 AngAng
1 Adam Trousdale
1 lpwisniewski
1 manojo
1 mentegy
1 mikhail
1 Mathias
1 msosnicki
1 Ang9876
1 Max Ovsiankin
1 Markus Kahl
1 Markus Hauck
1 Marc Karassev
1 Mads Hartmann
1 Lukas Ciszewski
1 Ang Hao Yang
1 Mike Samuel
1 Lucas Jen
1 Li Haoyi
1 Lanny Ripple
1 Mohuety Kirisame
1 Krzysiek Bochenek
1 phderome
1 Kevin Dreler
1 Keith Pinson
1 Kazuyoshi Kato
1 Kazuhiro Sera
1 Niklas Vest
1 Amadou CISSE
1 riiswa
1 Katrix
1 Karol Chmist
1 Ondra Pelech
Подробнее..

Изучаю Scala Часть 5 Http Requests

09.12.2020 04:19:27 | Автор: admin
Привет хабр! Продолжаю изучать Scala. Большинство бекендов так или иначе интегрированы с другими и делают HTTP запросы. Так как я на стек Cats и http4s ориентирован то буду рассматривать и изучать именно его. Сделаю запросы с куками, телом в json и в form, c файлом, с хедерами. Тут Hirrolot мне скорее всего минус поставит. Хочу сказать что может быть кому-то кто тоже изучает Scala будет полезна эта статья. Да и меня написание таких статей мотивирует изучать дальше. Люблю тебя малой. Расти большой не будь лапшой. Я уверен из тебя получится просто отличный инженер или даже может быть ученый в области IT. Давненько меня тут не было. В общем штормило у меня на личном фронте. С начала мы встречались обнимались и целовались с Марго. Потом мы расстались. Потом я переживал из-за этого. Потом работы навалилось. Вот так примерно у меня последние месяцы прошли. Взгрустнул, выпил и решил я написать сюда. И так, начнем.

Содержание



Ссылки


  1. Исходники
  2. Образы docker image
  3. Tapir
  4. Http4s
  5. Fs2
  6. Doobie
  7. ScalaTest
  8. ScalaCheck
  9. ScalaTestPlusScalaCheck


Тестовый контроллер который будет отвечать на наши запросы:
import cats.effect.{ContextShift, IO}import domain.todos.entities.Todoimport io.circe.generic.auto._import sttp.model.CookieWithMetaimport sttp.tapir.json.circe.jsonBodyimport sttp.tapir.{header, _}class TestController(implicit contextShift: ContextShift[IO]) extends ControllerBase {  private val baseTestEndpoint = baseEndpoint    .in("test")    .tag("Test")//Сюда мы будем делать наш запрос  private val postTest = baseTestEndpoint    .summary("Тестовый эндпойнт для запроска к самому себе")    .description("Возвращает тестовые данные")    .post    .in(header[String]("test_header"))    .in(jsonBody[List[Todo]])    .in(cookies)    .out(header[String]("test_header_out"))    .out(jsonBody[List[Todo]])    .out(setCookies)    .serverLogic(x => withStatus(IO {      (x._1 + x._3.map(c => c.name + "" + c.value).fold("")((a, b) => a + " " + b), x._2, List(CookieWithMeta(name = "test", value = "test_value")))    }))//Этот метод будет запускать наш запрос  private val runHttpRequestTes = baseTestEndpoint    .summary("Запускает тестовый запрос к самому себе")    .description("Запускает тестовый запрос к самому себе")    .get    .out(stringBody)    .serverLogic(_ => withStatus(runHttp()))  def runHttp(): IO[String] = {    ClientExamples.execute().as("Ok")  }  val endpoints = List(    postTest,    runHttpRequestTes  )}


Собственно сам запрос:
import cats.effect.{ContextShift, IO}import com.typesafe.scalalogging.StrictLoggingimport domain.todos.entities.Todoimport io.circe.generic.auto._import org.http4s.circe.CirceEntityCodec.circeEntityEncoderimport org.http4s.client.blaze._import org.http4s.client.middleware.Loggerimport org.http4s.headers._import org.http4s.{MediaType, Uri, _}import org.log4s._import java.time.Instantimport scala.concurrent.ExecutionContext.globalobject ClientExamples extends StrictLogging {  private[this] val logger = getLogger  def execute()(implicit contextShift: ContextShift[IO]) = {//Создаем клиент    BlazeClientBuilder[IO](global).resource.use { client =>      logger.warn("Start Request")//Оборачиваем его в мидлвар который будет логгировать запросы и ответы. //Указываем логгировать и боди и хедеры      val loggedClient = Logger[IO](true, true)(client)//Парсим адресс и небезопасным методом достаем результат      val uri = Uri.fromString("http://localhost:8080/api/v1/test").toOption.get//Создаем запрос. Указываем что это будет POST запрос по адресу что мы сформировали ранее      val request: Request[IO] = Request[IO](method = Method.POST, uri = uri)//Указываем что в json теле запроса передавать массив todo        .withEntity(List(Todo(1, "Test", 2, Instant.now())))//Указываем заголовки которые будут у запроса. Тут один наш кастомный.        .withHeaders(Accept(MediaType.application.json), Header(name = "test_header", value = "test_header_value"))//Указываем что с запросом будут оправляться куки с таким значением        .addCookie("test_cookie", "test_cookie_value")//Выполняем запрос      loggedClient.run(request).use(r => {        logger.warn("End Request")//Логгируем статус (200, 404, 500 и т.д)        logger.warn(r.status.toString())//Логгируем ответ        logger.warn(r.toString())//Пишем в логи хедеры ответа. Там в том числе есть Set-Cookie        logger.warn(r.headers.toString())//bodyText возвращает Stream[IO,String] и мы логгируем данные в нем//Можно десериализовать из этого json ответ сервера.        r.bodyText.map(t =>  logger.warn(t)).compile.drain      })    }  }}


В результате в логах увидим такой текст:
//Наш запрос 02:54:44.634 [ioapp-compute-7] INFO org.http4s.client.middleware.RequestLogger - HTTP/1.1 POST http://localhost:8080/api/v1/test Headers(Accept: application/json, test_header: test_header_value, Cookie: <REDACTED>) body="[{"id":1,"name":"Test","imageId":2,"created":"2020-12-08T23:54:44.627434500Z"}]"//Наш статус 02:54:44.641 [scala-execution-context-global-62] WARN appServices.ClientExamples - 200 OK//Наш ответ02:54:44.641 [scala-execution-context-global-62] WARN appServices.ClientExamples - Response(status=200, headers=Headers(test_header_out: test_header_value test_cookietest_cookie_value, Set-Cookie: <REDACTED>, Content-Type: application/json, Date: Tue, 08 Dec 2020 23:54:44 GMT, Content-Length: 79))//Хедеры нашего ответа02:54:44.641 [scala-execution-context-global-62] WARN appServices.ClientExamples - Headers(test_header_out: test_header_value test_cookietest_cookie_value, Set-Cookie: test=test_value, Content-Type: application/json, Date: Tue, 08 Dec 2020 23:54:44 GMT, Content-Length: 79)//Тело (json) нашего ответа сервера02:54:44.643 [ioapp-compute-6] WARN appServices.ClientExamples - [{"id":1,"name":"Test","imageId":2,"created":"2020-12-08T23:54:44.627434500Z"}]


Тут был специально показан запрос в максимально общем виде. Показано как установить куки, хедеры, тело запроса. Если нужно данные формы отправить или там файл то есть для этого пару способов. Сами данные методом .withEntity выставляются а вот объект формируется по другому
//Тут можно файл отправить через Part.fileData val data = Multipart(parts = Vector(Part.formData("age","18"):Part[IO]))//Или val data= UrlForm(("age","18"))//И создаем запрос  val request: Request[IO] = Request[IO](method = Method.POST, uri = uri)        .withEntity(data)
Подробнее..

Big Data Tools EAP 12 экспериментальная поддержка Python, поиск по ноутбукам в Zeppelin

16.12.2020 18:10:53 | Автор: admin

Только что вышло очередное обновление EAP 12 для плагина под названием Big Data Tools, доступного для установки в IntelliJ IDEA Ultimate, PyCharm Professional и DataGrip. Можно установить его через страницу плагина или внутри IDE. Плагин позволяет работать с Zeppelin, загружать файлы в облачные хранилища и проводить мониторинг кластеров Hadoop и Spark.


В этом релизе мы добавили экспериментальную поддержку Python и поиск по ноутбукам Zeppelin. Если вы страдали от каких-то багов, их тоже починено множество. Давайте поговорим об этих изменениях более подробно.



Экспериментальная поддержка Python в Zeppelin


Поддержку Python хотелось добавить давно. Несмотря на то, что PySpark в Zeppelin сейчас на волне хайпа, всё что предоставляет нам веб-интерфейс Zeppelin простейшее автодополнение, в котором содержится какой-то наполовину случайный набор переменных и функций. Вряд ли это можно ставить в вину Zeppelin, он никогда не обещал нам умного анализа кода. В IDE хочется видеть что-то намного большее.


Добавить целый новый язык звучит как очень сложная задача. К счастью, в наших IDE уже есть отличная поддержка Python: либо в PyCharm, либо в Python-плагине для IntelliJ IDEA. Нам нужно было взять эту готовую функциональность и интегрировать внутрь Zeppelin. Вместе с этим возникает много нюансов, специфичных для Zeppelin: как нам проанализировать список зависимостей, как найти правильную версию Python, и тому подобное.



Начиная с EAP 12, код на Python нормально подсвечивается в нашем редакторе ноутбуков Zeppelin, отображаются грубые синтаксические ошибки. Можно перейти на определение переменной или функции, если они объявлены внутри ноутбука. Можно сделать привычные рефакторинги вроде rename или change signature. Работают Zeppelin-специфичные таблицы и графики в конце концов, зачастую ради них люди и используют Zeppelin.



Конечно, многие вещи ещё предстоит сделать. Например, очень хотелось бы видеть умное автодополнение по функциям Spark API и другому внешнему коду. Сейчас мы нормально автодополняем только то, что написано внутри ноутбука. У нас есть хорошие идеи, как это реализовать в следующих релизах. Иначе говоря, не надо ждать какого-то чуда: теперь у вас есть полнофункциональный редактор Python, но это всё. Поддержка Python получилась довольно экспериментальной, но, как говорится, путь в тысячу ли начинается с первого шага. А ещё, даже имеющейся функциональности может оказаться достаточно, чтобы писать код в вашем любимом IDE и не переключаться на веб-интерфейс Zeppelin.


Смешиваем Scala и Python


Иногда, в одном и том же ноутбуке вам хочется одновременно использовать и Python, и Scala. Например, это бывает полезно из соображений производительности в вычислительных задачах.


Смешивать разные языки вполне возможно. Но не забывайте, что для полноценной поддержки Scala вам понадобится IntelliJ IDEA с плагинами Scala и Python. В PyCharm этот Scala-код хоть и будет выполняться, но его поддержка в редакторе останется на уровне plain text.



Поиск по ноутбукам Zeppelin


У нас всегда была возможность найти, в каком же файле на диске находится нужный нам текст (например, с помощью Find in Path, Ctrl+Shift+F). Но этот стандартный интерфейс поиска не работает с ноутбуками, ведь они не файлы!


Начиная с EAP 12 мы добавили отдельную панель поиска по ноутбукам. Откройте панель Big Data Tools, выделите какое-нибудь из подключений к Zeppelin и нажмите на кнопку с изображением лупы (или используйте сочетание клавиш Ctrl+F на клавиатуре). В результате, вы попадёте в окно под названием Find in Zeppelin Connections. Активация одного из результатов поиска приведет к открытию этого ноутбука и переходу на нужный параграф.



Похожий поиск есть и в веб-интерфейсе Zeppelin. Для получения результатов поиска мы используем стандартный HTTP API, поэтому результаты должны совпадать с тем, что вы видите в интерфейсе Zeppeliln по аналогичному поисковому запросу. Если вы раньше пользовались веб-интерфейсом и привыкли к поиску по ноутбукам, теперь он имеется и в Big Data Tools.


Функция небольшая, но очень полезная. Непонятно, как мы без неё жили раньше.


Исправления ошибок


Плагин Big Data Tools активно развивается, и при бурном росте неизбежны некоторые проблемы. В этом релизе мы провели много работы над правильной работой с удаленными хранилищами, отображением графиков и параграфов, переработали часть интерефейсов (например, SSH-туннели). Переработаны кое-какие системные вещи (например, несколько проектов теперь используют общее подключение к Zeppelin), вывезли множество ошибок в неожиданных местах. В целом, теперь пользоваться плагином намного приятней.


Если вам интересен обзор основных улучшений, то их можно найти в разделе What's New на странице плагина. Если вы ищете какую-то конкретную проблему, вам может подойти полный отчет из YouTrack.


Спасибо, что пользуетесь нашим плагином! Напоминаю, что установить свежую версию можно либо в браузере, на странице плагина, или внутри IDE по названию Big Data Tools. На странице плагина можно оставлять ваши отзывы и предложения (которые мы всегда читаем), и ставить оценки в виде звёздочек.


Документация и социальные сети


Ну и наконец, если вам нужно разобраться функциональностью Big Data Tools, у нас есть подробная документация в вариантах для IntelliJ IDEA и PyCharm. Если хочется задать вопрос, можно сделать это прямо в комментариях на Хабре или перейти в наш Twitter.


Надеемся, что все эти улучшения окажутся полезными, позволят вам делать более интересыне вещи, и более приятным способом.


Команда Big Data Tools

Подробнее..

Тестирование в Apache Spark Structured Streaming

02.01.2021 20:04:09 | Автор: admin

Введение


На текущий момент не так много примеров тестов для приложений на основе Spark Structured Streaming. Поэтому в данной статье приводятся базовые примеры тестов с подробным описанием.


Все примеры используют: Apache Spark 3.0.1.


Подготовка


Необходимо установить:


  • Apache Spark 3.0.x
  • Python 3.7 и виртуальное окружение для него
  • Conda 4.y
  • scikit-learn 0.22.z
  • Maven 3.v
  • В примерах для Scala используется версия 2.12.10.

  1. Загрузить Apache Spark
  2. Распаковать: tar -xvzf ./spark-3.0.1-bin-hadoop2.7.tgz
  3. Создать окружение, к примеру, с помощью conda: conda create -n sp python=3.7

Необходимо настроить переменные среды. Здесь приведен пример для локального запуска.


SPARK_HOME=/Users/$USER/Documents/spark/spark-3.0.1-bin-hadoop2.7PYTHONPATH=$SPARK_HOME/python:$SPARK_HOME/python/lib/py4j-0.10.9-src.zip;

Тесты


Пример с scikit-learn


При написании тестов необходимо разделять код таким образом, чтобы можно было изолировать логику и реальное применение конечного API. Хороший пример изоляции: DataFrame-pandas, DataFrame-spark.


Для написания тестов будет использоваться следующий пример: LinearRegression.


Итак, пусть код для тестирования использует следующий "шаблон" для Python:


class XService:    def __init__(self):        # Инициализация    def train(self, ds):        # Обучение    def predict(self, ds):        # Предсказание и вывод результатов

Для Scala шаблон выглядит соответственно.


Полный пример:


from sklearn import linear_modelclass LocalService:    def __init__(self):        self.model = linear_model.LinearRegression()    def train(self, ds):        X, y = ds        self.model.fit(X, y)    def predict(self, ds):        r = self.model.predict(ds)        print(r)

Тест.


Импорт:


import unittestimport numpy as np

Основной класс:


class RunTest(unittest.TestCase):

Запуск тестов:


if __name__ == "__main__":    unittest.main()

Подготовка данных:


X = np.array([    [1, 1],  # 6    [1, 2],  # 8    [2, 2],  # 9    [2, 3]  # 11])y = np.dot(X, np.array([1, 2])) + 3  # [ 6  8  9 11], y = 1 * x_0 + 2 * x_1 + 3

Создание модели и обучение:


service = local_service.LocalService()service.train((X, y))

Получение результатов:


service.predict(np.array([[3, 5]]))service.predict(np.array([[4, 6]]))

Ответ:


[16.][19.]

Все вместе:


import unittestimport numpy as npfrom spark_streaming_pp import local_serviceclass RunTest(unittest.TestCase):    def test_run(self):        # Prepare data.        X = np.array([            [1, 1],  # 6            [1, 2],  # 8            [2, 2],  # 9            [2, 3]  # 11        ])        y = np.dot(X, np.array([1, 2])) + 3  # [ 6  8  9 11], y = 1 * x_0 + 2 * x_1 + 3        # Create model and train.        service = local_service.LocalService()        service.train((X, y))        # Predict and results.        service.predict(np.array([[3, 5]]))        service.predict(np.array([[4, 6]]))        # [16.]        # [19.]if __name__ == "__main__":    unittest.main()

Пример с Spark и Python


Будет использован аналогичный алгоритм LinearRegression. Нужно отметить, что Structured Streaming основан на тех же DataFrame-х, которые используются и в Spark Sql. Но как обычно есть нюансы.


Инициализация:


self.service = LinearRegression(maxIter=10, regParam=0.01)self.model = None

Обучение:


self.model = self.service.fit(ds)

Получение результатов:


transformed_ds = self.model.transform(ds)q = transformed_ds.select("label", "prediction").writeStream.format("console").start()return q

Все вместе:


from pyspark.ml.regression import LinearRegressionclass StructuredStreamingService:    def __init__(self):        self.service = LinearRegression(maxIter=10, regParam=0.01)        self.model = None    def train(self, ds):        self.model = self.service.fit(ds)    def predict(self, ds):        transformed_ds = self.model.transform(ds)        q = transformed_ds.select("label", "prediction").writeStream.format("console").start()        return q

Сам тест.


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


train_ds = spark.createDataFrame([    (6.0, Vectors.dense([1.0, 1.0])),    (8.0, Vectors.dense([1.0, 2.0])),    (9.0, Vectors.dense([2.0, 2.0])),    (11.0, Vectors.dense([2.0, 3.0]))],    ["label", "features"])

Это очень удобно и код получается компактным.


Но подобный код, к сожалению, не будет работать в Structured Streaming, т.к. созданный DataFrame не будет обладать нужными свойствами, хотя и будет соответствовать контракту DataFrame.
На текущий момент для создания источников для тестов можно использовать такой же подход, что и в тестах для Spark.


def test_stream_read_options_overwrite(self):    bad_schema = StructType([StructField("test", IntegerType(), False)])    schema = StructType([StructField("data", StringType(), False)])    df = self.spark.readStream.format('csv').option('path', 'python/test_support/sql/fake') \        .schema(bad_schema)\        .load(path='python/test_support/sql/streaming', schema=schema, format='text')    self.assertTrue(df.isStreaming)    self.assertEqual(df.schema.simpleString(), "struct<data:string>")

И так.


Создается контекст для работы:


spark = SparkSession.builder.enableHiveSupport().getOrCreate()spark.sparkContext.setLogLevel("ERROR")

Подготовка данных для обучения (можно сделать обычным способом):


train_ds = spark.createDataFrame([    (6.0, Vectors.dense([1.0, 1.0])),    (8.0, Vectors.dense([1.0, 2.0])),    (9.0, Vectors.dense([2.0, 2.0])),    (11.0, Vectors.dense([2.0, 3.0]))],    ["label", "features"])

Обучение:


service = structure_streaming_service.StructuredStreamingService()service.train(train_ds)

Получение результатов. Для начала считываем данные из файла и выделяем: признаки и идентификатор для объектов. После запускаем предсказание с ожиданием в 3 секунды.


def extract_features(x):    values = x.split(",")    features_ = []    for i in values[1:]:        features_.append(float(i))    features = Vectors.dense(features_)    return featuresextract_features_udf = udf(extract_features, VectorUDT())def extract_label(x):    values = x.split(",")    label = float(values[0])    return labelextract_label_udf = udf(extract_label, FloatType())predict_ds = spark.readStream.format("text").option("path", "data/structured_streaming").load() \    .withColumn("features", extract_features_udf(col("value"))) \    .withColumn("label", extract_label_udf(col("value")))service.predict(predict_ds).awaitTermination(3)

Ответ:


15.9669918.96138

Все вместе:


import unittestimport warningsfrom pyspark.sql import SparkSessionfrom pyspark.sql.functions import col, udffrom pyspark.sql.types import FloatTypefrom pyspark.ml.linalg import Vectors, VectorUDTfrom spark_streaming_pp import structure_streaming_serviceclass RunTest(unittest.TestCase):    def test_run(self):        spark = SparkSession.builder.enableHiveSupport().getOrCreate()        spark.sparkContext.setLogLevel("ERROR")        # Prepare data.        train_ds = spark.createDataFrame([            (6.0, Vectors.dense([1.0, 1.0])),            (8.0, Vectors.dense([1.0, 2.0])),            (9.0, Vectors.dense([2.0, 2.0])),            (11.0, Vectors.dense([2.0, 3.0]))        ],            ["label", "features"]        )        # Create model and train.        service = structure_streaming_service.StructuredStreamingService()        service.train(train_ds)        # Predict and results.        def extract_features(x):            values = x.split(",")            features_ = []            for i in values[1:]:                features_.append(float(i))            features = Vectors.dense(features_)            return features        extract_features_udf = udf(extract_features, VectorUDT())        def extract_label(x):            values = x.split(",")            label = float(values[0])            return label        extract_label_udf = udf(extract_label, FloatType())        predict_ds = spark.readStream.format("text").option("path", "data/structured_streaming").load() \            .withColumn("features", extract_features_udf(col("value"))) \            .withColumn("label", extract_label_udf(col("value")))        service.predict(predict_ds).awaitTermination(3)        # +-----+------------------+        # |label|        prediction|        # +-----+------------------+        # |  1.0|15.966990887541273|        # |  2.0|18.961384020443553|        # +-----+------------------+    def setUp(self):        warnings.filterwarnings("ignore", category=ResourceWarning)        warnings.filterwarnings("ignore", category=DeprecationWarning)if __name__ == "__main__":    unittest.main()

Нужно отметить, что для Scala можно воспользоваться созданием потока в памяти.
Это может выглядеть вот так:


implicit val sqlCtx = spark.sqlContextimport spark.implicits._val source = MemoryStream[Record]source.addData(Record(1.0, Vectors.dense(3.0, 5.0)))source.addData(Record(2.0, Vectors.dense(4.0, 6.0)))val predictDs = source.toDF()service.predict(predictDs).awaitTermination(2000)

Полный пример на Scala (здесь, для разнообразия, не используется sql):


package aaa.abc.dd.spark_streaming_pr.clusterimport org.apache.spark.ml.regression.{LinearRegression, LinearRegressionModel}import org.apache.spark.sql.DataFrameimport org.apache.spark.sql.functions.udfimport org.apache.spark.sql.streaming.StreamingQueryclass StructuredStreamingService {  var service: LinearRegression = _  var model: LinearRegressionModel = _  def train(ds: DataFrame): Unit = {    service = new LinearRegression().setMaxIter(10).setRegParam(0.01)    model = service.fit(ds)  }  def predict(ds: DataFrame): StreamingQuery = {    val m = ds.sparkSession.sparkContext.broadcast(model)    def transformFun(features: org.apache.spark.ml.linalg.Vector): Double = {      m.value.predict(features)    }    val transform: org.apache.spark.ml.linalg.Vector => Double = transformFun    val toUpperUdf = udf(transform)    val predictionDs = ds.withColumn("prediction", toUpperUdf(ds("features")))    predictionDs      .writeStream      .foreachBatch((r: DataFrame, i: Long) => {        r.show()        // scalastyle:off println        println(s"$i")        // scalastyle:on println      })      .start()  }}

Тест:


package aaa.abc.dd.spark_streaming_pr.clusterimport org.apache.spark.ml.linalg.Vectorsimport org.apache.spark.sql.SparkSessionimport org.apache.spark.sql.execution.streaming.MemoryStreamimport org.scalatest.{Matchers, Outcome, fixture}class StructuredStreamingServiceSuite extends fixture.FunSuite with Matchers {  test("run") { spark =>    // Prepare data.    val trainDs = spark.createDataFrame(Seq(      (6.0, Vectors.dense(1.0, 1.0)),      (8.0, Vectors.dense(1.0, 2.0)),      (9.0, Vectors.dense(2.0, 2.0)),      (11.0, Vectors.dense(2.0, 3.0))    )).toDF("label", "features")    // Create model and train.    val service = new StructuredStreamingService()    service.train(trainDs)    // Predict and results.    implicit val sqlCtx = spark.sqlContext    import spark.implicits._    val source = MemoryStream[Record]    source.addData(Record(1.0, Vectors.dense(3.0, 5.0)))    source.addData(Record(2.0, Vectors.dense(4.0, 6.0)))    val predictDs = source.toDF()    service.predict(predictDs).awaitTermination(2000)    // +-----+---------+------------------+    // |label| features|        prediction|    // +-----+---------+------------------+    // |  1.0|[3.0,5.0]|15.966990887541273|    // |  2.0|[4.0,6.0]|18.961384020443553|    // +-----+---------+------------------+  }  override protected def withFixture(test: OneArgTest): Outcome = {    val spark = SparkSession.builder().master("local[2]").getOrCreate()    try withFixture(test.toNoArgTest(spark))    finally spark.stop()  }  override type FixtureParam = SparkSession  case class Record(label: Double, features: org.apache.spark.ml.linalg.Vector)}

Выводы


При написании тестов необходимо разделять код таким образом, чтобы разделять логику и применение конкретных вызовов API. Можно использоваться любые доступные источники. В том числе и kafka.


Такие абстракции как DataFrame позволяют это сделать легко и просто.


При использовании Python данные придется хранить в файлах.


Ссылки и ресурсы


Подробнее..
Категории: Scala , Python , Testing , Apache , Spark , Apache spark , Kafka , Streaming

Автоматическая генерация type classes в Scala 3

07.01.2021 16:12:32 | Автор: admin

В Scala широко используется подход к наделению классов дополнительной функциональностью, называемый type classes. Для тех, кто никогда не сталкивался с этим подходом рекомендую почитать вот эту статью. Этот подход позволяет держать код каких-то аспектов функционирования класса отдельно от самой реализации класса. И создавать его даже не имея доступа к коду самого класса. В частности, такой подход оправдан и рекомендуем при наделении классов возможностью сериализации/десериализации в определенный формат. Например библиотека работы с Json из фреймворка Play использует type classes для задания правил представления объектов в json формате.

Если type class предназначен для использования в большом количестве разнообразных классов (как например при сериализации/десериализации), то писать код type class для каждого класса с которым он должен работать нерационально и трудозатратно. Во многих случаях можно сгенерировать реализацию type class автоматически зная набор атрибутов класса для которого он предназначается. К сожалению в текущей версии scala автоматическая генерация type class затруднена. Она требует либо самостоятельного написания макросов, либо использования сторонних фреймворков для генерации type class таких как shapeless или magnolia, которые также основаны на макросах.

В Scala 3, которая стремительно движется к релизу появилась встроенная в язык возможность автоматической генерации type class. В этой статье делается попытка разобраться с использованием этого механизма на примере конкретного type class.

Объявление type class

В качестве примера будет использоваться достаточно искусственный type class который мы назовем Inverter. Он будет содержать один метод:

trait Inverter[T] {  def invert(value: T): T}

Предполагается что он будет принимать значение и каким либо образом "инвертировать" его. Для строк под инвертированием мы будем понимать изменение порядка символов на противоположный, для чисел - изменение знака числа, а для логического типа - применение логического NOT. Для всех структурных типов инвертированием будет инвертирование значений всех их атрибутов. Такой type class имеет мало практического смысла, но для нас он хорош тем, что позволяет продемонстрировать как обработку входящего значения базового класса так и выдачу результата в виде значения базового класса.

Итак первое что нужно сделать - это определить type class для элементарных типов. Делается это объявлением given значений (аналог implicit из Scala 2) с реализацией Inverter в объекте компаньоне Inverter:

object Inverter {  given Inverter[String] = new Inverter[String] {    override def invert(str: String): String =      str.reverse  }  given Inverter[Int] = new Inverter[Int] {    override def invert(value: Int): Int =      -value  }    given Inverter[Boolean] = new Inverter[Boolean] {    override def invert(value: Boolean): Boolean =      !value  }  }

Теперь займемся автоматической генерацией Inverter для сложных типов. Для того чтобы автоматическая генерация была возможна необходимо объявить в объекте компаньоне метод derived[T] возвращающий Inverter[T]. Реализация этого метода может быть любой. Например можно генерировать type class с помощью макроса или при помощи высокоуровневой библиотеки генерации (например shapeless 3). Нас же будет интересовать генерация через встроенный низкоуровневый механизм. Для его работы метод derived должен получить контекстный параметр типа Mirror.Of[T]. Этот параметр позволит нам получить информацию о структуре нашего типа. Параметр Mirror.Of[T] генерируется компилятором автоматически для следующих типов:

  • case классы и case объекты

  • перечисления (enum и enum cases)

  • sealed trait-ы единственными наследниками которых являются case классы и case объекты.

Собственно этот список это и есть тот список классов для которых может автоматически генерироваться type class при использовании описываемого механизма.

Сразу нужно отметить что это не runtime механизм получения информации о типе во время исполнения, а compile time механизм использующий новый фичи Scala 3 по кодогенерации (это объясняет, в частности, то, что большинство методов генерации должны объявляться как inline).

Приведем реализацию метода derived для нашего случая. В первом варианте реализации мы будем реализовывать только генерацию для case классов и объектов (а также кортежей).

  inline def derived[T](using m: Mirror.Of[T]): Inverter[T] = {    val elemInstances = summonAll[m.MirroredElemTypes]    inline m match {      case p: Mirror.ProductOf[T] =>        productInverter[T](p, elemInstances)      case s: Mirror.SumOf[T] => ???    }  }  inline def summonAll[T <: Tuple]: List[Inverter[_]] =    inline erasedValue[T] match      case _: EmptyTuple => List()      case _: (t *: ts) => summonInline[Inverter[t]] :: summonAll[ts]

Разберемся что здесь происходит. Для всех структурных типов Miror.Of[T] позволяет определить типы элементов класса через MirroredElemTypes. Для случая case классов и кортежей это просто типы всех полей. Поскольку для инвертирования нашего типа нам надо инвертировать все его поля, то нам необходимо получить экземпляры Inverter для всех типов полей нашего класса. Это делается через метод summonAll. Реализация summonAll использует новый механизм поиска given значений summonInline. Мы сейчас не будет останавливаться на тонкостях этой реализации, так как для наших целей реализация метода summonAll будет всегда одинаковой независимо от того какой type class мы генерируем.

После получения списка Inverter для всех элементов класса мы определяем чем является наш класс - произведением других классов (case классы, case объекты, кортежи) или суммой (sealed trait или enum). Поскольку сейчас нас интересуют только случай произведения, то для этого случая вызывается метод productInverter, который создает имплементацию Inverter на основе Inverter для всех элементов класса:

def productInverter[T](p: Mirror.ProductOf[T], elems: List[Inverter[_]]): Inverter[T] = {    new Inverter[T] {      def invert(value: T): T = {        val oldValues = value.asInstanceOf[Product].productIterator        val newValues = oldValues.zip(elems)          .map { case (value, inverter) =>            inverter.asInstanceOf[Inverter[Any]].invert(value)          }          .map(_.asInstanceOf[AnyRef])          .toArray        p.fromProduct(Tuple.fromArray(newValues))      }    }  }

Реализация этого метода делает следующее. Во-первых, получается список значений всех полей экземпляра класса. Так как мы знаем что наш класс является произведением типов то он реализует trait Product, который позволяет получить итератор всех значений полей. Во-вторых список значений полей объединяется со списком Inverter для них и для каждого поля к значению применяется свой Inverter. Наконец, в-третьих, из списка инвертированных значений собирается новый экземпляр класса. За эту сборку отвечает метод fromProduct доступный через Mirror объект.

Использование derived

Каким же образом теперь можно использовать созданный метод derived для получения type class для конечного класса. Тут есть несколько подходов. Самый простой - использовать при объявлении case класса конструкцию derives которая указывает что для этого класса необходимо сгенерировать указанный type class. Вот пример такого объявления:

case class Sample(intValue: Int, stringValue: String, boolValue: Boolean) derives Inverter

После этого экземпляр Inverter[Sample] будет сгенерирован и доступен везде где виден класс Sample. Далее мы просто можем получать его через summon и использовать:

println(summon[Inverter[Sample]].invert(Sample(1, "abc", false)))// Результат: Sample(-1,cba,true)

Такой подход можно использовать если у нас есть полный доступ к классам для которых нам необходимы type class и мы хотим явно заявлять в них эту новую функциональность.

Однако часто мы не хотим или не можем в явном виде модифицировать класс, для которого нам нужен type class. В этом случае мы можем объявить given значение для этого класса пользуясь методом derived:

case class Sample(intValue: Int, stringValue: String, boolValue: Boolean)@main def mainProc = {    given Inverter[Sample] = Inverter.derived  println(summon[Inverter[Sample]].invert(Sample(1, "abc", false)))  // Результат: Sample(-1,cba,true)  } 

Нужно однако понимать что такая генерация type class является полуавтоматической. Для вложенных case классов type class автоматически сгенерирован не будет. Скажем для иерархии:

case class InnerSample(s: String)case class OuterSample(inner: InnerSample)

необходимо будет последовательно сгенерировать необходимые type class:

  given Inverter[InnerSample] = Inverter.derived  given Inverter[OuterSample] = Inverter.derived  println(summon[Inverter[OuterSample]].invert(OuterSample(InnerSample("abc"))))  // Результат: OuterSample(InnerSample(cba))

В большинстве случаев однако можно разрешить компилятору автоматически генерировать type class для всех типов для которых доступен Mirror.Of. Для этого просто объявляем универсальный given:

  inline given[T] (using m: Mirror.Of[T]): Inverter[T] = Inverter.derived[T]    println(summon[Inverter[OuterSample]].invert(OuterSample(InnerSample("abc"))))  // Результат: OuterSample(InnerSample(cba))

По какому пути идти и насколько автоматизировать генерацию type class в каждом конкретном случае нужно решать индивидуально. Разработчикам библиотек я бы рекомендовал прятать автоматическую генерацию в отдельный trait или объект, которые можно подключить через наследование (или import соответственно) там, где это необходимо:

trait AutoInverting {  inline given[T] (using m: Mirror.Of[T]): Inverter[T] = Inverter.derived[T]}

Кастомные type class и автоматическая генерация

Автоматическая генерация type class вполне допускает использование type class созданных вручную для тех типов где нужна специальная реализация. При этом при эти созданные вручную реализации будут использоваться в том числе и там где они входят в качестве элементов в другие классы.

Например рассмотрим следующую иерархию case классов:

case class SampleUnprotected(value: String)case class SampleProtected(value: String)case class Sample(prot: SampleProtected, unprot: SampleUnprotected)

Допустим для класса SampleProtected мы хотим иметь специальную реализацию Inverter, которая не инвертирует его поле value. Посмотрим как будет это сочетаться с автоматической генерацией type class для Sample:

  inline given[T] (using m: Mirror.Of[T]): Inverter[T] = Inverter.derived[T]    given Inverter[SampleProtected] = new Inverter[SampleProtected] {    override def invert(prot: SampleProtected): SampleProtected = SampleProtected(prot.value)  }    println(summon[Inverter[Sample]].invert(Sample(SampleProtected("abc"), SampleUnprotected("abc"))))  // Результат: Sample(SampleProtected(abc),SampleUnprotected(cba))

Как видим Inverter автоматически сгенерированный для класса Sample подхватил кастомную реализацию Inverter для SampleProtected. Это позволяет определять в библиотеке автоматическую генерацию и все равно оставлять пользователю возможность делать кастомные реализации там где это необходимо.

Обработка sealed trait и enum

Помимо генерации type class для case классов (и прочих произведений классов) можно генерировать type class и для sealed trait иерархий. Для этого в методе derived необходимо дописать ветку, отвечающую за сумму классов:

  inline def derived[T](using m: Mirror.Of[T]): Inverter[T] = {    val elemInstances = summonAll[m.MirroredElemTypes]    inline m match {      case p: Mirror.ProductOf[T] =>        productInverter[T](p, elemInstances, getFields[p.MirroredElemLabels])      case s: Mirror.SumOf[T] =>         sumInverter(s, elemInstances)    }  }  def sumInverter[T](s: Mirror.SumOf[T], elems: List[Inverter[_]]): Inverter[T] = {    new Inverter[T] {      def invert(value: T): T = {        val index = s.ordinal(value)        elems(index).asInstanceOf[Inverter[Any]].invert(value).asInstanceOf[T]      }    }  }

Посмотрим что здесь происходит. В случае суммы типов типы элементы в Mirror определяют типы наследников от базового типа которые и могут выступать типом нашего экземпляра. Для того чтобы определить какой именно тип элемента нужно использовать нужно воспользоваться методом ordinal из Mirror. Он возвращает индекс типа элемента который соответствует текущему значению экземпляра. Далее мы берем соответствующий Inverter (выбирая его из списка по этому индексу) и используем для инвертирования нашего экземпляра.

Посмотрим как это работает на простейших примерах. Мы не будем создавать собственную иерархию с sealed trait а воспользуемся уже готовыми классами Either и Option:

def checkInverter[T](value: T)(using inverter: Inverter[T]): Unit = {  println(s"$value => ${inverter.invert(value)}")}  @main def mainProc = {    inline given[T] (using m: Mirror.Of[T]): Inverter[T] = Inverter.derived[T]    given Inverter[SampleProtected] = new Inverter[SampleProtected] {    override def invert(prot: SampleProtected): SampleProtected = SampleProtected(prot.value)  }    val eitherSampleLeft: Either[SampleProtected, SampleUnprotected] = Left(SampleProtected("xyz"))  checkInverter(eitherSampleLeft)  // Результат: Left(SampleProtected(xyz)) => Left(SampleProtected(xyz))  val eitherSampleRight: Either[SampleProtected, SampleUnprotected] = Right(SampleUnprotected("xyz"))  checkInverter(eitherSampleRight)  // Результат: Right(SampleUnprotected(xyz)) => Right(SampleUnprotected(zyx))  val optionalValue: Option[String] = Some("123")  checkInverter(optionalValue)  // Результат: Some(123) => Some(321)  val optionalValue2: Option[String] = None  checkInverter(optionalValue2)  // Результат: None => None  checkInverter((6, "abc"))  // Результат: (6,abc) => (-6,cba)}

Здесь мы для наглядности выделили использование Inverter в отдельный метод чтобы показать автоматическую генерацию type class без явного указания summon. Как видно генерация работает правильно и для Either и для опционального типа и для кортежей (они на самом деле обрабатываются не веткой SumOf, а веткой ProductOf).

Использование наименований полей класса

Вернемся к вопросу генерации type class для случая произведения классов и рассмотрим еще один аспект, который может оказаться важным для задач сериализации/десериализации. В нашем примере реализация инвертирования зависела только от типов полей класса, но не от названий этих полей. Однако во многих случаях реализация type class должна будет использовать наименования полей. Чтобы продемонстрировать как это можно делать введем в наш пример генерации Inverter еще одно требование: те поля класса наименование которых начинается на два символа подчеркивания инвертирование выполняться не должно. Попробуем реализовать это требование. Для этого нам понадобится реализовать метод получения списка названий полей и поправить реализацию derived:

  inline def derived[T](using m: Mirror.Of[T]): Inverter[T] = {    val elemInstances = summonAll[m.MirroredElemTypes]    inline m match {      case p: Mirror.ProductOf[T] =>        productInverter[T](p, elemInstances, getFields[p.MirroredElemLabels])      case s: Mirror.SumOf[T] =>         sumInverter(s, elemInstances)    }  }  inline def getFields[Fields <: Tuple]: List[String] =    inline erasedValue[Fields] match {      case _: (field *: fields) => constValue[field].toString :: getFields[fields]      case _ => List()    }  def productInverter[T](p: Mirror.ProductOf[T], elems: List[Inverter[_]], labels: Seq[String]): Inverter[T] = {    new Inverter[T] {      def invert(value: T): T = {        val newValues = value.asInstanceOf[Product].productIterator          .zip(elems).zip(labels)          .map { case ((value, inverter), label) =>            if (label.startsWith("__"))              value            else              inverter.asInstanceOf[Inverter[Any]].invert(value)          }          .map(_.asInstanceOf[AnyRef])          .toArray        p.fromProduct(Tuple.fromArray(newValues))      }    }  }

Проверим как работает такая реализация на следующем классе:

case class Sample(value: String, __hidden: String)

Для такого класса должно инвертироваться значение value, но не должно инвертироваться значение __hidden:

  inline given[T] (using m: Mirror.Of[T]): Inverter[T] = Inverter.derived[T]    println(summon[Inverter[Sample]].invert(Sample("abc","abc")))  // Результат: Sample(cba,abc)

Выводы

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

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

Подробнее..
Категории: Scala , Macro , Dotty , Type class

Приглашаем на DINS SCALA EVENING Cassandra4io, Calypso, Higher Kinded Data

02.03.2021 12:21:43 | Автор: admin

На митапе Сергей Рублев из DINS расскажет, как они с командой написали легковесную библиотеку с типизированными запросами в doobie-like стиле. Ахтям Сакаев из компании Метр квадратный поговорит о Calypso Scala-библиотеке для удобной работы с BSON. Олег Нижников из Tinkoff.ru рассмотрит паттерн Higher Kinded Data. Участие бесплатное, но нужно зарегистрироваться.

Подробная программа и информация о спикерах под катом.


Программа

19:00-19:40 Cassandra4io: легковесная doobie-like библиотека (Сергей Рублев, DINS)

Сергей расскажет, почему его команда отказалась от существующих библиотек для Cassandra, и как написала свою. Вы узнаете, как реализовать типизацию параметров запросов и ответов с помощью строковой интерполяции и shapeless.

Доклад будет интересен Junior и Middle разработчикам.

Сергей Рублев тимлид в DINS. В индустрии больше 10 лет. Начинал изучение функционального программирования с Erlang и Clojure, но в итоге остановился на Scala. В основном работал с аналитикой, немного с e-commerce.

19:40-20:20 Calypso: Scala-библиотека для удобной работы с BSON (Ахтям Сакаев, Метр квадратный)

Calypso библиотека для работы с BSON в Scala. Она использует type-directed programming, поэтому компилятор выводит новые кодеки сам. Calypso предлагает кодеки на основе type class для сопоставления между структурами данных Scala и BSON.

В этом выступлении мы рассмотрим общие принципы разработки функциональных библиотек на Scala. Затем углубимся в дизайн и реализацию Calypso, совместимость с cats и refined.

Ахтям Сакаев ведущий инженер в компании Метр Квадратный. Увлечен распределенными системами и функциональным программированием.

20:20-21:10 Выпекаем типы данных с HKD (Олег Нижников, Tinkoff.ru)

Вместе с Олегом рассмотрим паттерн функционального программирования под названием Higher Kinded Data. Обсудим, как HKD позволяет избавиться от бойлерплейта и нетипизированного кода. Доклад содержит примеры кода на Scala 3.

Доклад будет интересен людям, которые используют Scala в своих приложениях.

Олег Нижников архитектор в Tinkoff.ru. Пишет на Scala, делает opensource-библиотеки и любит общаться на тему функционального программирования.

Как присоединиться

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

Как проходят встречи

Записи предыдущих митапов можно посмотреть на нашем YouTube-канале.

О нас

DINS IT EVENING это место встречи и обмена знаниями технических специалистов по направлениям Java, DevOps, QA и JS. Несколько раз в месяц мы организуем встречи, чтобы обсудить с коллегами из разных компаний интересные кейсы и темы. Открыты для сотрудничества, если у вас есть наболевший вопрос или тема, которой хочется поделиться пишите на itevening@dins.ru!

Подробнее..

Scala Selenium. Сколько человек в сборной имеют более одного гражданства?

27.03.2021 12:17:15 | Автор: admin

Рассмотрим пример использования Selenium на Scala, отвечая на вопрос "Сколько человек в каждой футбольной сборной имеют более одного гражданства?"

За основу возьмем данные с сайта transfermarkt.com:

List of football/soccer teams

Переход на заданную страницу, если она реализует trait org.scalatestplus.selenium.Page, может осуществляться так:

import org.scalatestplus.selenium.Pageimport org.scalatestplus.selenium.WebBrowser._import org.openqa.selenium.WebDriverimplicit def webDriver: WebDriver = ??? /* from container */class RankingListPage(implicit val webDriver: WebDriver) extends Page {   val url = "https://www.transfermarkt.com/statistik/weltrangliste/statistik"}val rankingListPage = new RankingListPage()go to rankingListPage

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

Xpath локатор кнопки будет таким:

import org.scalatestplus.selenium.WebBrowser._val compactTab: Query = xpath("//div[.='Compact']")

Ожидание видимости элемента осуществляется так (timeout можно задать в конфиге, query - заданный элемент):

import org.openqa.selenium._import org.openqa.selenium.support.ui.WebDriverWaitimport org.openqa.selenium.support.ui.ExpectedConditionsimport org.scalatestplus.selenium.WebBrowser._import java.time.Durationdef waitVisible(query: Query, timeout: Int)(implicit webDriver: WebDriver): WebElement =    new WebDriverWait(webDriver, Duration.ofSeconds(timeout)).until(ExpectedConditions.visibilityOfElementLocated(query.by))

Рассмотрим переход на закладку Compact.

Переход на закладку будет состоять из следующих шагов:

  • Проверяем, активна ли закладка (активная закладка в данном случае в атрибуте class содержит "active").

  • Если да, то ничего не делаем переход осуществлен.

  • Если нет, то кликаем на закладку и ждём, когда закладка станет активна.

Проверить, что элемент query: Query содержит заданное значение в атрибуте class можно так:

def doesClassContain(value: String): Boolean =    (for {      element   <- find(query)      attribute <- element.attribute("class")    } yield attribute.contains(value)).contains(true)

Кликнуть на элемент можно так: clickOn(query)

Ожидание, когда атрибут class элемента будет содержать заданное значение, можно реализовать так:

def waitClassContain(value: String): Boolean =  new WebDriverWait(driver, Duration.ofSeconds(timeout)).until(ExpectedConditions.attributeContains(query.by, "class", value))

Итого:

def clickCompact(): Unit =    if (!compactTab.doesClassContain("active")) {      clickOn(compactTab)      val _ = compactTab.waitClassContain("active")    }

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

Для этого нужно последовательно обойти все страницы рейтинга и с каждой считать список.

Логика будет такой:

  • Проверяем, достигли ли последней страницы

  • Если нет, считываем список со страницы, переходим на следующую и возвращаемся на пункт выше

  • Если дошли до последней страницы, то считываем список с неё

Для того, чтобы проверить, достигли ли мы последней страницы, достаточно проверить, есть ли кнопка перехода на следующую страницу (см. скрин выше, css локатор li.naechste-seite > a):

val nextPageLink: Query = cssSelector("li.naechste-seite > a")def isPresent: Boolean = find(nextPageLink).isDefined

Для того, чтобы считать список стран, необходимо найти все элементы с xpath локатором //table/tbody//a[count(*)=0] и у каждого элемента считать text и значение атрибута href (или //table/tbody/tr[td[.='CONMEBOL']]//a[count(*)=0] - если интересна только одна конфедерация, например, самая маленькая - CONMEBOL(Южная Америка)):

val itemLink: Query = xpath("//table/tbody//a[count(*)=0]")def items(): Seq[(String, Option[String])] =  findAll(itemLink).map(el => (el.text.trim, el.attribute("href"))).toSeq

Для перехода на следующую страницу мало кликнуть на кнопку nextPageLink, необходимо ещё дождаться, когда этот переход произойдет.

Чтобы удостовериться, что мы перешли на следующую страницу, мы можем считать номер текущей страницы (css локатор li.selected > a), а после клика на nextPageLink дождаться, когда номер текущей страницы станет на 1 больше:

val selectedPageLink: Query = cssSelector("li.selected > a")def clickNextPage(): Unit = {  val nextPage = find(selectedPageLink).map(_.text).get().toInt + 1  clickOn(nextPageLink)  val _ = webDriverWait(driver).until(ExpectedConditions.textToBe(selectedPageLink.by, nextPage.toString))}

Соединяем все воедино и получаем следующий список (на 13 марта 2021):

Теперь, когда у нас есть url страны можно составить список игроков сборной.

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

Ссылка на страницу игрока - это xpath локатор //table/tbody//span[@class='hide-for-small']/a[count(*)=0]:

val itemLink: Query = xpath("//table/tbody//span[@class='hide-for-small']/a[count(*)=0]")

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

Для начала нужно перейти на закладку Profile.

Мы уже переходили на закладку на предыдущих страницах. Здесь будет то же самое, за исключением одного нюанса: если раньше у активной закладки менялся атрибут class,то теперь этот атрибут меняется не у самой ссылки, а у её родителя. Кстати, в этот раз немецкие разработчики сайта значение атрибута class активного элемента не стали переводить на английский, а оставили на немецком - "aktiv":

Создадим два элемента: ссылку и её родителя, а затем определим переход на закладку так:

val profileTab: Query  = xpath("//li[@id='profile']")val profileLink: Query = xpath("//li[@id='profile']/a")def clickProfile(): Unit =   if (!profileTab.doesClassContain("aktiv")) {    clickOn(profileLink)    val _ = profileTab.waitClassContain("aktiv")  }

Теперь осталось только определить гражданство. Для этого возьмем элемент img из строки Citizenship: и считаем её атрибут title:

val citizenshipImg: Query = xpath("//th[.='Citizenship:']/following-sibling::td/img")def citizenship(): Seq[String] = findAll(citizenshipImg).flatMap(_.attribute("title")).toSeq

Вот и все, все страницы заполнены, осталось только написать автотест и тогда получим следующий результат.

Results (for Russia, Ukraine and Belarus)

Country name

%

Foreigners

Russia

11% (3/28)

(Brazil (1) -> (Mrio Fernandes), Kyrgyzstan (1) -> (Ilzat Akhmetov), Germany (1) -> (Roman Neustdter))

Ukraine

9% (3/33)

(Brazil (2) -> (Marlos, Jnior Moraes), Hungary (1) -> (Igor Kharatin))

Belarus

4% (1/25)

(Cameroon (1) -> (Maks Ebong))

В наших сборных только 3 натурализованных игрока (и все из Бразилии). Остальные родились в СССР.

Results (for CONMEBOL)

Country name

%

Foreigners

Brazil

36% (9/25)

(Spain (3) -> (Casemiro, Bruno Guimares, Vincius Jnior), Italy (1) -> (Alex Telles), France (1) -> (Thiago Silva), Portugal (4) -> (Ederson, Marquinhos, Allan, Lucas Paquet))

Argentina

57% (13/23)

(Spain (2) -> (Gonzalo Montiel, Lionel Messi), Italy (11) -> (Lucas Martnez Quarta, Wlter Kannemann, Nicols Tagliafico, Guido Rodrguez, Rodrigo de Paul, Giovani Lo Celso, Nicols Domnguez, ngel Di Mara, Joaqun Correa, Papu Gmez, Lucas Alario))

Uruguay

51,5% (18/35)

(Spain (7) -> (Jos Mara Gimnez, Sebastin Coates, Diego Godn, Agustn Oliveros, Damin Surez, Lucas Torreira, Federico Valverde), Paraguay (1) -> (Rodrigo Muoz), Italy (10) -> (Fernando Muslera, Martn Campaa, Sergio Rochet, Matas Via, Franco Pizzichillo, Nahitan Nndez, Matas Vecino, Giorgian de Arrascaeta, Diego Rossi, Cristhian Stuani))

Colombia

22% (6/27)

(Spain (4) -> (Jeison Murillo, Johan Mojica, James Rodrguez, Luis Surez), Argentina (1) -> (Frank Fabra), England (1) -> (Steven Alzate))

Chile

21% (5/24)

(Haiti (1) -> (Jean Beausejour), Spain (3) -> (Claudio Bravo, Gary Medel, Fabin Orellana), Italy (1) -> (Luis Jimnez))

Peru

33% (12/36)

(Venezuela (1) -> (Carlos Ascues), Spain (3) -> (Alexander Callens, Cristian Benavente, Sergio Pea), Uruguay (1) -> (Gabriel Costa), Italy (2) -> (Luis Abram, Gianluca Lapadula), Netherlands (1) -> (Renato Tapia), Switzerland (1) -> (Jean-Pierre Rhyner), Portugal (1) -> (Andr Carrillo), Croatia (1) -> (Ral Ruidaz), Lebanon (1) -> (Matas Succar))

Venezuela

25% (7/28)

(Spain (4) -> (Roberto Rosales, Juanpi Aor, Darwin Machs, Fernando Aristeguieta), Switzerland (1) -> (Rolf Feltscher), England (1) -> (Luis Del Pino Mago), Colombia (1) -> (Jan Hurtado))

Paraguay

21% (7/33)

(Spain (1) -> (Antonio Sanabria), Argentina (4) -> (Santiago Arzamendia, Gastn Gimnez, Andrs Cubas, Ral Bobadilla), Italy (2) -> (Antony Silva, Ivn Piris))

Ecuador

12% (4/33)

(Spain (3) -> (Erick Ferigra, Pervis Estupin, Leonardo Campana), Argentina (1) -> (Hernn Galndez))

Bolivia

25% (7/28)

(United States (2) -> (Adrin Jusino, Antonio Bustamante), Spain (1) -> (Jaume Cullar), Argentina (1) -> (Carlos Lampe), Brazil (1) -> (Marcelo Moreno), Switzerland (1) -> (Boris Cespedes), Portugal (1) -> (Erwin Snchez))

А вот в Южной Америке людей с двойным гражданством довольно много. Впрочем, это неудивительно: в чемпионатах Евросоюза жесткий лимит на легионеров (в заявке только 3 игрока с гражданством не ЕС), поэтому южноамериканцам, чтобы попасть в Европу, приходится либо пытаться получить гражданство бывшей митрополии (Бразилия -> Португалия, остальные -> Испания), либо искать среди своих предков итальянцев. Второе не так сложно, как кажется. Во время Второй Мировой войны Южная Америка хоть и была на бумаге нейтральной, по факту разделилась на два лагеря: Бразилия -> союзники, Аргентина + Уругвай -> фашисты. Поэтому неудивительно, что после 1945 года многие итальянцы в поисках лучшей жизни иммигрировали из разрушенной фашисткой Италии в симпатизировавшим ей Аргентине и Уругваю. Поэтому современному аргентинцу или уругвайцу получить гражданство Италии не сложнее, чем человеку по фамилии Зильберман - гражданство Израиля - кто-нибудь среди предков нужной национальности да найдётся!

Source code

Подробнее..

Scala Selenium. Самый стремительный взлет в Лиги наций УЕФА?

01.04.2021 20:04:02 | Автор: admin

Какой самый стремительный взлет в Лиги наций УЕФА?

Т.к. цель этой статьи показать, как пишутся Selenium-автотесты на Scala, а не поставить интригующий вопрос, продержать читателя в неведении, а в конце статьи дать неожиданный ответ, то дальше будут спойлеры.

С момента запуска Лиги наций УЕФА прошло целых два розыгрыша и уже можно подвести промежуточные результаты:

  • России два раза подряд занимала второе место в группе дивизиона B (оба раза блестяще начиная и столь же блестяще-позорно заканчивая) и в третьем розыгрыше предсказуемо выступит в том же дивизионе B - вот это стабильность (со слезами на глазах)

  • Сборная Турции два раза подряд занимала последнее место в своей группе (оба раза в той же самой, где была и сборная России), но в третьем розыгрыше выступит всего лишь одним дивизионом ниже - в дивизионе С

  • А вот и обещанный спойлер: сборная Венгрии в первом розыгрыше заняла второе место в третьем по силе дивизионе С, но в третьем розыгрыше выступит в самом сильном дивизионе А вместе с топ-сборными - вот это феерический взлет. Жаль что он произошел в той же самой группе, где была и сборная России.

А как же выступили остальные сборные? После того, как интрига была напрочь уничтожена, можно заняться автоматизацией. За основу возьмем данные с сайта transfermarkt.com: результаты сборных в Лиге наций

Определимся с понятиями

В лиге наций УЕФА 4 по силе дивизиона: A (самый сильный), B, C, D. Победитель дивизиона поднимается в более сильный дивизион, занявший последнее место - опускается дивизионом ниже. В первом розыгрыше в сильных дивизионах было только по 3 команды, но после этого УЕФА решила, что топ-матчей должно быть больше и во втором розыгрыше во всех дивизионах, кроме самого слабого (D), стало выступать по 4 команды. Именно по причине необходимой доукомплектации более сильных дивизионов после первого розыгрыша Турция, занявшая последнее место, не вылетела, а Венгрия, занявшая второе - поднялась выше.

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

Страница дивизиона Лиги Наций УЕФА

Для того, чтобы перейти на страницу группы X необходимо выполнить следующее:

  • Перейти на главную страницу сайта transfermarkt.com

  • В верхнем меню нажать на пункт Competitions

  • Во всплывающем меню выбрать пункт UEFA Nations League X

Xpath локаторы для ссылок меню Competitions и UEFA Nations League X будут следующие:

val competitionsLink: Query         = xpath("//a[normalize-space(.)='Competitions']")def groupLink(group: String): Query = xpath(s"//a[normalize-space(.)='UEFA Nations League $group']")

Мы вынуждены использовать функцию normalize-space, потому что ссылка меню Competitions содержит не только слово Competitions, но ещё и кучу пробелов с одним переносом строки:

Именно поэтому в локаторе //a[normalize-space(.)='Competitions'] мы ищем элемент с тэгом a, текстовое содержимое которого после нормализации пробелов равно Competitions.

Ожидание появления элемента query можно реализовать так:

new WebDriverWait(driver, Duration.ofSeconds(timeout)).until(ExpectedConditions.visibilityOfElementLocated(query.by))

в результате вернется видимый элемент WebElement, на который затем можно кликнуть и получится:

competitionsLink.waitVisible().click()groupLink(group).waitVisible().click()

Конечно, после перехода на страницу группы Лиги Наций нужно подождать, когда эта страница загрузится. Для этого возьмем элемент заглавия страницы с css-локатором div.dataName > h1:

и подождем, когда в нём отобразится название группы:

val header: Query = cssSelector("div.dataName > h1")new WebDriverWait(driver, Duration.ofSeconds(timeout)).until(ExpectedConditions.textToBe(header.by, s"UEFA Nations League $group"))

Результаты в дивизионе

В правом нижнем углу страницы дивизиона есть результаты выступлений сборных:

Строка с результатами сборной будет иметь xpath-локатор //table//tr[td[@class='rechts']]. Всего у нас будет до 16 строк и каждую строку нужно обработать - выудить из строки место сборной (оно необходимо, чтобы определить состав дивизионов в третьем розыгрыше) и, конечно, название страны. К сожалению, Scala библиотека для Selenium довольно бедная и не позволяет искать подэлементы относительно заданного, поэтому воспользуемся Java-библиотекой:

val resultRow: Query = xpath("//table//tr[td[@class='rechts']]")import scala.jdk.CollectionConverters._import org.openqa.selenium._val webDriver: WebDriver = ???def results: mutable.Buffer[(Int, String)] =  webDriver.findElements(resultRow.by).asScala.map { el =>    {      val place = el.findElement(By.xpath(".//td[@class='rechts']")).getText      val name  = el.findElement(By.xpath(".//td[contains(@class, 'hauptlink')]")).getText      (place.toInt, name)    }  }

Здесь происходит следующее:

  • находим все элементы с xpath-локатором //table//tr[td[@class='rechts']]

  • конвертируем java.util.List в scala.collection.mutable.Buffer

  • для каждого элемента из первого пункта находим два его дочерних подэлемента: .//td[@class='rechts'] - место в таблице, .//td[contains(@class, 'hauptlink')] - название страны

  • считываем текст из дочерних элементов

Получим следующую картину, например, для дивизиона D:

Place

Country

1

Faroe Islands

2

Malta

3

Latvia

4

Andorra

1

Gibraltar

2

Liechtenstein

3

San Marino

Previous results

Для того, чтобы получить результаты предыдущего сезона, необходимо в фильтре Filter by season: выбрать значение 18/19 и нажать на кнопку Show:

Здесь будет больше действий и проверок:

  • В первую очередь нужно раскрыть список доступных сезонов (нажать на кнопку с css-локатором a.chzn-single > div > b)

  • Затем кликнуть на предыдущий сезон (нажать на ссылку с xpath-локатором //li[.='18/19'])

  • После этого необходимо дождаться, когда скроется список доступных сезонов

  • Затем необходимо нажать на кнопку Show (css-локатор input[value='Show'])

  • И подождать, когда произойдет переход на страницу предыдущего сезона (для этого будем ждать, когда url текущей страницы станет оканчиваться на saison_id=2018 (url на немецком))

val selectSeason: Query   = cssSelector("a.chzn-single > div > b")val previousSeason: Query = xpath("//li[.='18/19']")val show: Query           = cssSelector("input[value='Show']")def selectPreviousSeason: Boolean = {  selectSeason.waitVisible().click()  previousSeason.waitVisible().click()  new WebDriverWait(driver, Duration.ofSeconds(timeout)).until(invisibilityOfElementLocated(previousSeason.by))  clickOn(show)  new WebDriverWait(driver, Duration.ofSeconds(timeout)).until(wd => wd.getCurrentUrl.endsWith("saison_id=2018"))}

Теперь можно собрать все воедино и получим следующее:

  • Переходим на главную страницу сайта

  • Для каждой из групп 'A', 'B', 'C', 'D' выполняем:

  • Переходим в заданную группу

  • Считываем результаты второго розыгрыша

  • Сохраняем их

  • Переходим в предыдущий сезон

  • Считываем результаты первого розыгрыша

  • Сохраняем их

P.S. Забегая вперед, скажу, что за это время Macedonia сменила имя на North Macedonia - это пришлось учесть.

case class Result(number: Int, group: Char, place: Int, country: String)val mainPage = new MainPagego to mainPageval results: ArrayBuffer[Result] = ArrayBuffer.empty[Result]Seq('A', 'B', 'C', 'D').foreach(group => {  val leagueGroupPage = mainPage.goToGroup(group.toString)  val groupResult     = leagueGroupPage.results  groupResult.foreach { case (place, country) => results += Result(2, group, place, country) }  leagueGroupPage.selectPreviousSeason  leagueGroupPage.waitLoad(group.toString)  val previousSeasonResult = leagueGroupPage.results  previousSeasonResult.foreach {    case (place, country) =>      results += Result(1, group, place, country.replace("Macedonia", "North Macedonia"))  }})

Теперь осталось только обработать результаты.

Результаты у нас в виде коллекции case class Result(number: Int, group: Char, place: Int, country: String), приведем эту коллекцию к коллекции case class ParsedResult(country: String, firstSeason: (Char, Int), secondSeason: (Char, Int), thirdSeason: Char, progress: (Int, Int)), где сезон представлен в виде tuple (дивизион, итоговое место), а прогресс - из двух цифр, обозначающих прогресс по итогам розыгрыша: 1 (повышение в классе) | 0 | -1 (понижение)

import scala.collection.mutable.ArrayBuffercase class Result(number: Int = 0, group: Char = 'E', place: Int = 0, country: String = "")case class ParsedResult(country: String,                        firstSeason: (Char, Int),                        secondSeason: (Char, Int),                        thirdSeason: Char,                        progress: (Int, Int))val results: ArrayBuffer[Result] = ???val parsedResults = results        .groupBy(_.country)        .view        .mapValues(seq => {          val country: String           = seq.head.country          val firstRes                  = seq.find(_.number == 1).getOrElse(Result())          val firstSeason: (Char, Int)  = (firstRes.group, firstRes.place)          val secondRes                 = seq.find(_.number == 2).getOrElse(Result())          val secondSeason: (Char, Int) = (secondRes.group, secondRes.place)          val thirdSeason: Char =            if (secondSeason._2 == 1 && secondSeason._1 != 'A') (secondSeason._1 - 1).toChar            else if (secondSeason._2 == 4 && secondSeason._1 != 'D') (secondSeason._1 + 1).toChar            else secondSeason._1          val progress: (Int, Int) = (firstSeason._1 - secondSeason._1, secondSeason._1 - thirdSeason)          ParsedResult(country, firstSeason, secondSeason, thirdSeason, progress)        })        .values        .groupBy(_.progress)

Самые успешные сборные?

Разделим полученный результат на группы сборных, объединенных по достигнутому прогрессу.

Получим следующий результат:

Суперпрогресс (сборные, совершившие прорыв через два дивизиона) - 2

Country

1st

2nd

3rd

Hungary

C(2)

B(1)

A

Armenia

D(2)

C(1)

B

Поистине феерическое выступление показывают Венгрия и Армения: и вот уже сборная из самого низшего дивизиона будет выступать в той же лиге, что и Россия.

Поднялись и закрепились - 13

Country

1st

2nd

3rd

Denmark

B(1)

A(2)

A

Romania

C(2)

B(3)

B

Scotland

C(1)

B(2)

B

Finland

C(1)

B(2)

B

Israel

C(2)

B(3)

B

Norway

C(1)

B(2)

B

Luxembourg

D(2)

C(2)

C

Azerbaijan

D(2)

C(3)

C

Georgia

D(1)

C(3)

C

Belarus

D(1)

C(2)

C

Kosovo

D(1)

C(3)

C

North Macedonia

D(1)

C(2)

C

и т.д. Полный список результатов тут. А исходный код тут.

Предыдущая статья: Scala + Selenium. Сколько человек в сборной имеют более одного гражданства?

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru