Току-що приключих с четенето на Въведение във функционалното програмиране чрез ламбда смятане отГрег Майкълсън. Към края авторът преподава малко SML и Lisp. Попаднах на няколко кратки функции, които се възползваха от структурния модел на SML, съответстващ на синтаксис и рекурсия, две много важни функционални характеристики, които може да видите и в по-широко използвани FP езици като Erlang, Haskell, Rust, F#, за да назовем само няколко. Помислих си защо не JavaScript? Въпреки че JavaScript няма естествена поддръжка за съпоставяне на шаблони, имаше някои смели опити да се направи това чрез библиотеки на потребителската област като:



Въпреки това не мислех, че наистина носят голяма стойност. Други проекти изглеждаха наистина застояли. Най-добре би било, разбира се, да се въведе съвпадение на шаблони естествено в JavaScript, както е посочено в следното предложение:



Призовавам ви да подкрепите този проект от етап 0, тъй като той определено предизвиква разговора и вниманието, което заслужава. Независимо от това, JavaScript измина дълъг път с поддръжката за „деструктуриране“, която можете да мислите като много елементарно съвпадение на шаблон и добра отправна точка за това ново предложение.

И така, реших да използвам JavaScript, за да видя дали мога да имитирам изразителността в SML, за да подходя към някои тривиални функции по този функционален начин.

Първо, малко за синтаксиса на SML, който ще видите, ако не сте запознати. SML е статично въведен език, така че цяло число i се декларира като i: int, списък от низове t като t: string list. И накрая, операторът за конкатенация infix ::, който ще върне нов списък с първия обект в горна позиция, а другият в опашка, напр. 0::[1,2,3] -> [0,1,2,3] . За упражненията, публикувани тук, това е степента, в която ще използвате синтаксиса на SML.

Ще трябва да намеря аналог на JavaScript на тези езикови функции. Както казах преди, деструктурирането ми позволява лесно да разложа масив на глава и опашка, като използвам [h, … tail]като заместител на [h::t]. Това ще направи рекурсивните решения много по-лесни. Що се отнася до типовете, използвах „Flow“. С Flow число i се декларира като i: number, а поредица от низове t като t: Array<string>. За да се доближа до SML, ще декларирам псевдоним тип Flow:

type list<E> = Array<E>

И това е всичко, наистина! Нека да си проправим път там:

Пример 1: функции на две падежни клаузи

Тези примери демонстрират прости рекурсивни алгоритми със съвпадение на два шаблона в стила на регистъра: основа и рекурсивна стъпка. Имайте предвид, че под капака съпоставянето на шаблони не е нищо повече от условно разклоняване отгоре надолу, съответстващо на структурата и формата на предоставените аргументи. Когато се намери съвпадение, функцията изпълнява израза отдясно. В SML това би изглеждало така:

fun length [] = 0 |
    length (_::(t:int list)) = 1 + (length t);

Има 2 израза за стил на регистър. Ако извикате length с празен масив, се връща нула. В противен случай проверете следващата клауза за случай (разделена с |), която ще съответства на непразен масив. В този случай главата се съпоставя със заместващ знак _ и се изхвърля, накрая рекурсивното извикване изчислява 1 плюс дължината на опашката на списъка.

Сега, престорете се, че няма свойство Array.prototype.length в JavaScript за момент, можем да изразим това като:

const length = ([h, ...tail]: list<number>): number =>
   isNil(h) ? 0
            : 1 + length(tail)
assert.equal(length([]), 0)
assert.equal(length([1,2,3]), 3)

Структурно смятам, че тази функция запазва същата изразителност на клаузата за съвпадение на шаблона отляво с върнатата стойност отдясно. Мислете за всеки троичен оператор a ? b : c като свързващ заедно case клаузите — съпоставете a, ако true изпълнете b, в противен случай изпълнете c. Освен четивността, особено ми харесва, че ни подтиква да използваме рекурсия на опашката и неизменност!

В същия дух можем да създадем други малки програми със същия модел:

Сума на елементи в списък:

const sum = ([h, ...tail]: list<number>): number =>
  isNil(h) ? 0
           : h + sum(tail)
assert.equal(sum([]), 0)
assert.equal(sum([1,2,3]), 6)

Отпечатайте първите n кубчета, започвайки от 0:

const cubes = (n: number): list<number> =>
  !n ? [0]
     : cubes(n - 1).concat([n * n * n])
assert.deepEqual(cubes(0), [0])
assert.deepEqual(cubes(3), [0, 1, 8, 27])

И така, всичко това е забавно и хубаво, но този модел нарушава ли › клаузите с 2 случая?

Пример 2: функции на три падежни клаузи

Ще внедря функция, която намира i-тия елемент в списъка с низове. В SML:

fun sifind _ [] = "can't find it" |
    sifind 0 ((h:string)::_) = h  |
    sifind (i:int) (_::(t:string list)) = sifind (i - 1) t;

Това е красотата и силата на съпоставянето на шаблони в това, че е толкова декларативно, че можете да прочетете алгоритъма извън екрана: опитайте се да намерите която и да е позиция в празен списък и ще получите грешка; или опитайте да намерите първия низ в списъка, след което получавате главата на списъка; накрая, продължете да търсите в останалата част от списъка.

Нека внедрим това в JavaScript, така че да се чете по същия начин. Позволих си да добавя малко повече отблясъци и да декларирам функция find, която ще намери i-тия елемент в списък с обекти mixed. Типът на връщането е Either за по-добро представяне както на неуспех, така и на успех.

const find = (i: number, [h, ...tail]: list<mixed>): Either<string, number> 
   =>
      isNil(h) ? Either.Left(`Element not found at ${i}`)
     : i === 0 ? Either.Right(h)
               : find(i - 1, tail)
assert.isTrue(find(0, []).isLeft)
assert.isTrue(find(1, [0]).isLeft)
assert.isTrue(find(2, ['a', 'b', 'c']).isRight)    assert.equal(find(2, ['a', 'b', 'c']).fold(null, identity), 'c')

Нека добавим още един пример за клауза с 3 случая. Този път ще напиша функция за замяна на i-тия елемент от списък с друг елемент. Трябва да започнете да виждате модел в тези:

const replace = (i: number, e: mixed, [h, ...tail]: list<mixed>):   
list<mixed> 
  =>
     isNil(h) ? []
    : i === 0 ? [e, ...tail]
    : [h].concat(replace(i - 1, e, tail))

assert.deepEqual(replace(2, 'Jo',
      ['Chris', 'Jean', 'Les', 'Pat', 'Phil']),
      ['Chris', 'Jean', 'Jo', 'Pat', 'Phil']
)

Надяваме се, че сте забелязали друго функционално качество на всички тези програми — неизменност. В този конкретен случай замяната на стойност никога не променя оригиналния масив.

Нека да разгледаме един последен пример, функция за вмъкване на списък с общо предназначение:

const insert = (e: any, [h, ...tail]: list<any>): list<mixed> =>
   isNil(h) ? [e]
    : e < h ? [e, h, ...tail]
            : [h].concat(insert(e, tail))
assert.deepEqual(insert('Jo',
      ['Chris', 'Jean', 'Les', 'Pat', 'Phil']),
      ['Chris', 'Jean', 'Jo', 'Les', 'Pat', 'Phil']
    )
assert.deepEqual(insert('Jo', []), ['Jo'])

В този случай реших да използвам списък от any, така че претовареният оператор за сравнение e < h да работи, точно както в нетипизирания JS.

Разбира се, бих предпочел да видя по-декларативен синтаксис за съвпадение на шаблони, вместо да се налага да използвам if then else, но ако се замислите, зад кулисите това работи някак си.

Това е всичко, което исках да споделя. Мислех, че е наистина страхотно, че можем да разсъждаваме и да изразяваме код по този начин, използвайки JavaScript. Това е свидетелство за това колко ковък е езикът. Мисля, че определено върви в правилната посока и трябва да продължим да настояваме за тези ориентирани към FP предложения колкото е възможно повече.

Благодаря за четенето!

P.S. Ако имате пример за › 3 функция за „съвпадение на шаблони“ в JS, моля, не се колебайте да оставите коментар :-)

Ресурси

  • Въведение във функционалното програмиране чрез ламбда смятане, Грег Майкълсън