)) } } }

Scala - Опция Monad / Може би Monad

import language.higherKinds trait Monad[M[_]] { def pure[A](a: A): M[A] def flatMap[A, B](ma: M[A])(f: A => M[B]): M[B] def map[A, B](ma: M[A])(f: A => B): M[B] = flatMap(ma)(x => pure(f(x))) } object Monad { def apply[F[_]](implicit M: Monad[F]): Monad[F] = M implicit val myOptionMonad = new Monad[MyOption] { def pure[A](a: A) = MySome(a) def flatMap[A, B](ma: MyOption[A])(f: A => MyOption[B]): MyOption[B] = ma match { case MyNone => MyNone case MySome(a) => f(a) } } } sealed trait MyOption[+A] { def flatMap[B](f: A => MyOption[B]): MyOption[B] = Monad[MyOption].flatMap(this)(f) def map[B](f: A => B): MyOption[B] = Monad[MyOption].map(this)(f) } case object MyNone extends MyOption[Nothing] case class MySome[A](x: A) extends MyOption[A]

Започваме с прилагане на Monad клас, който ще бъде основата за всички наши реализации на монада. Наличието на този клас е много удобно, защото чрез прилагане само на два от методите му - pure и flatMap - за конкретна монада ще получите много методи безплатно (ние ги ограничаваме само до метода map в нашите примери, но като цяло има много други полезни методи, като sequence и traverse за работа с масиви от Monad s).

Можем да изразим map като състав на pure и flatMap. Можете да видите от подписа на flatMap $ flatMap: (T до M [U]) до (M [T] до M [U]) $, че е наистина близо до $ map: (T до U) до (M [T] до M [U]) $. Разликата е в допълнителните $ M $ в средата, но можем да използваме pure функция за конвертиране на $ U $ в $ M [U] $. По този начин изразяваме map по отношение на flatMap и pure.

Това работи добре за Scala, защото има усъвършенствана система от тип. Също така работи добре за JS, Python и Ruby, тъй като те се въвеждат динамично. За съжаление не работи за Swift, защото е статично напечатан и няма разширени функции от типа като висши видове , така че за Swift ще трябва да внедрим map за всяка монада.

Също така имайте предвид, че Option monad вече е a де факто стандарт за езици като Swift и Scala, така че използваме малко по-различни имена за нашите реализации на монада.

Сега, когато имаме база Monad клас, нека да стигнем до нашите реализации на Option monad. Както бе споменато по-горе, основната идея е, че Option или притежава някаква стойност (наречена Some), или изобщо не съдържа стойност (None).

pure метод просто повишава стойност до Some, докато flatMap метод проверява текущата стойност на Option - ако е None тогава се връща None и ако е Some с базисна стойност, тя извлича основната стойност, прилага f() към него и връща резултат.

Имайте предвид, че само като използвате тези две функции и map, е невъзможно да влезете в изключение на нулев указател - никога. (Проблемът бих могъл потенциално възникват при нашето изпълнение на flatMap метод, но това са само няколко реда в нашия код, които проверяваме веднъж. След това просто използваме нашата реализация на Option monad в целия ни код на хиляди места и изобщо не трябва да се страхуваме от изключението от нулевия указател.)

Или монадата

Нека да се потопим във втората монада: Или. Това по същество е същото като Option monad, но с Some наречен Right и None наречен Left. Но този път, Left също е позволено да има основна стойност.

Имаме нужда от това, защото е много удобно да изразим хвърляне на изключение. Ако възникне изключение, тогава стойността на Either ще бъде Left(Exception). flatMap функцията не напредва, ако стойността е Left, което повтаря семантиката на хвърляне на изключения: Ако се случи изключение, спираме по-нататъшното изпълнение.

JavaScript - или Monad

import Monad from './monad'; export class Either extends Monad { // pure :: a -> Either a pure = (value) => { return new Right(value) } // flatMap :: # Either a -> (a -> Either b) -> Either b flatMap = f => this.isLeft() ? this : f(this.value) isLeft = () => this.constructor.name === 'Left' } export class Left extends Either { constructor(value) { super(); this.value = value; } toString() { return `Left(${this.value})` } } export class Right extends Either { constructor(value) { super(); this.value = value; } toString() { return `Right(${this.value})` } } // attempt :: (() -> a) -> M a Either.attempt = f => { try { return new Right(f()) } catch(e) { return new Left(e) } } Either.pure = (new Left(null)).pure

Python - или Monad

from monad import Monad class Either(Monad): # pure :: a -> Either a @staticmethod def pure(value): return Right(value) # flat_map :: # Either a -> (a -> Either b) -> Either b def flat_map(self, f): if self.is_left: return self else: return f(self.value) class Left(Either): def __init__(self, value): self.value = value self.is_left = True class Right(Either): def __init__(self, value): self.value = value self.is_left = False

Руби - или Монада

require_relative './monad' class Either Either a def self.pure(value) Right.new(value) end # pure :: a -> Either a def pure(value) self.class.pure(value) end # flat_map :: # Either a -> (a -> Either b) -> Either b def flat_map(f) if is_left self else f.call(value) end end end class Left

Суифт - или Монада

import Foundation enum Either { case Left(A) case Right(B) static func pure(_ value: C) -> Either { return Either.Right(value) } func flatMap(_ f: (B) -> Either) -> Either { switch self { case .Left(let x): return Either.Left(x) case .Right(let x): return f(x) } } func map(f: (B) -> C) -> Either { return self.flatMap { Either.pure(f(

Опция / Може би, и двете, и бъдещите монади в JavaScript, Python, Ruby, Swift и Scala



Този урок за монади дава кратко обяснение на монадите и показва как да внедрите най-полезните в пет различни езика за програмиране - ако търсите монади в JavaScript , монади в Python , монади в Руби , монади в Бързо , и / или монади в Стълба , или за да сравните всякакви реализации, четете правилната статия!

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



Ето какво обхващам по-долу:



  • Въведение в теорията на категориите
  • Определението за монада
  • Реализации на опцията („Може би“) монада, или монада, и бъдеща монада, плюс примерна програма, използваща ги, в JavaScript, Python, Ruby, Swift и Scala

Да започваме! Първата ни спирка е теорията на категориите, която е в основата на монадите.



Въведение в теорията на категориите

Теорията на категориите е математическа област, която се развива активно в средата на 20 век. Сега това е основата на много концепции за функционално програмиране, включително монадата. Нека разгледаме набързо някои концепции за теория на категориите, настроени за терминология за разработване на софтуер.

Така че има три основни концепции, които определят a категория:



  1. Тип е точно такъв, какъвто го виждаме на статично типизирани езици. Примери: Int, String, Dog, Cat и др.
  2. Функции свържете два вида. Следователно те могат да бъдат представени като стрелка от един тип към друг тип или към себе си. Функция $ f $ от тип $ T $ до тип $ U $ може да се обозначи като $ f: T до U $. Можете да го възприемате като функция на езика за програмиране, която приема аргумент от тип $ T $ и връща стойност от тип $ U $.
  3. Състав е операция, обозначена с оператора $ cdot $, която изгражда нови функции от съществуващи. В категория винаги е гарантирано за всякакви функции $ f: T to U $ и $ g: U to V $ съществува уникална функция $ h: T to V $. Тази функция се обозначава като $ f cdot g $. Операцията ефективно преобразува чифт функции в друга функция. В езиците за програмиране тази операция е, разбира се, винаги възможна. Например, ако имате функция, която връща дължина на низ - $ strlen: String to Int $ —и функция, която казва дали числото е четно - $ even: Int to Boolean $ - тогава можете да направите функция $ even { _} strlen: String to Boolean $, която казва дали дължината на String е равномерно. В този случай $ even { _} strlen = even cdot strlen $. Съставът предполага две характеристики:
    1. Асоциативност: $ f cdot g cdot h = (f cdot g) cdot h = f cdot (g cdot h) $
    2. Съществуването на функция за идентичност: $ forall T: съществува f: T до T $, или на обикновен английски, за всеки тип $ T $ съществува функция, която съответства $ T $ на себе си.

Така че нека да разгледаме проста категория.

Проста категория, включваща String, Int и Double, и някои функции сред тях.



Странична бележка: Предполагаме, че Int, String и всички останали типове тук са гарантирани, че не са нулеви, т.е. нулевата стойност не съществува.

Странична бележка 2: Това всъщност е само част на категория, но това е всичко, което искаме за нашата дискусия, тъй като тя има всички основни части, от които се нуждаем, и диаграмата е по-малко претрупана по този начин. Реалната категория също би имала всички съставени функции като $ roundToString: Double to String = intToString cdot round $, за да удовлетвори клаузата за композиция на категориите.



Може да забележите, че функциите в тази категория са супер прости. Всъщност е почти невъзможно да има грешка в тези функции. Няма нули, няма изключения, а само аритметиката и работата с паметта. Така че единственото лошо нещо, което може да се случи, е повреда на процесора или паметта - в този случай все пак трябва да сринете програмата, но това се случва много рядко.

Не би ли било хубаво, ако целият ни код просто работи на това ниво на стабилност? Абсолютно! Но какво да кажем за I / O, например? Определено не можем да живеем без него. Тук на помощ идват решенията на монадите: Те изолират всички нестабилни операции в супер малки и много добре одитирани парчета код - тогава можете да използвате стабилни изчисления в цялото си приложение!



Въведете Monads

Нека наречем нестабилно поведение като I / O a страничен ефект . Сега искаме да можем да работим с всички наши предварително дефинирани функции като length и типове като String по стабилен начин в присъствието на това страничен ефект .

Така че нека да започнем с празна категория $ M [A] $ и да я превърнем в категория, която ще има стойности с определен тип страничен ефект, а също и стойности без странични ефекти. Да приемем, че сме определили тази категория и тя е празна. В момента няма нищо полезно, което можем да направим с него, така че, за да го направим полезен, ще следваме следните три стъпки:



  1. Попълнете го със стойности на типовете от категория $ A $, като String, Int, Double и др. (Зелени полета в диаграмата по-долу)
  2. След като имаме тези стойности, все още не можем да направим нищо смислено с тях, затова се нуждаем от начин да вземем всяка функция $ f: T to U $ от $ A $ и да създадем функция $ g: M [T] до M [U] $ (сини стрелки в диаграмата по-долу). След като имаме тези функции, можем да направим всичко със стойностите в категория $ M [A] $, което успяхме да направим в категория $ A $.
  3. Сега, когато имаме чисто нова категория $ M [A] $, се появява нов клас функции с подпис $ h: T to M [U] $ (червени стрелки в диаграмата по-долу). Те се появяват в резултат на популяризиране на ценности в първа стъпка като част от нашата кодова база, т.е. ние ги записваме при необходимост; това са основните неща, които ще разграничат работата с $ M [A] $ спрямо работата с $ A $. Последната стъпка ще бъде да накараме тези функции да работят добре и за типове в $ M [A] $, т.е. да можем да извлечем функция $ m: M [T] до M [U] $ от $ h: T до M [U] $

Създаване на нова категория: Категории A и M [A], плюс червена стрелка от A

Така че нека да започнем, като дефинираме два начина за повишаване на стойности от $ A $ типове до стойности от $ M [A] $ типове: една функция без странични ефекти и една със странични ефекти.

  1. Първият се нарича $ pure $ и се дефинира за всяка стойност на стабилна категория: $ pure: T to M [T] $. Получените стойности на $ M [T] $ няма да имат странични ефекти, поради което тази функция се нарича $ pure $. Например, за I / O монада, $ pure $ веднага ще върне някаква стойност, без възможност за неуспех.
  2. Вторият се нарича $ constructor $ и за разлика от $ pure $ връща $ M [T] $ с някои странични ефекти. Пример за такъв $ конструктор $ за асинхронна I / O монада може да бъде функция, която извлича някои данни от мрежата и ги връща като String. Стойността, върната от $ constructor $, в този случай ще има тип $ M [String] $.

Сега, когато имаме два начина за промотиране на ценности в $ M [A] $, от вас като програмист зависи да изберете коя функция да използвате, в зависимост от целите на вашата програма. Нека разгледаме пример тук: Искате да извлечете HTML страница като https://www.toptal.com/javascript/option-maybe-either-future-monads-js и за това правите функция $ fetch $. Тъй като всичко може да се обърка при извличането му - помислете за мрежови грешки и т.н. - ще използвате $ M [String] $ като тип на връщане на тази функция. Така че ще изглежда нещо като $ fetch: String to M [String] $ и някъде в тялото на функцията там ще използваме $ constructor $ за $ M $.

Сега да приемем, че правим фалшива функция за тестване: $ fetchMock: String to M [String] $. Той все още има същия подпис, но този път просто инжектираме получената HTML страница в тялото на $ fetchMock $, без да правим никакви нестабилни мрежови операции. Така че в този случай ние просто използваме $ pure $ в изпълнението на $ fetchMock $.

Като следваща стъпка се нуждаем от функция, която безопасно повишава произволна функция $ f $ от категорията $ A $ до $ M [A] $ (сини стрелки в диаграма). Тази функция се нарича $ map: (T to U) to (M [T] to M [U]) $.

Сега имаме категория (която може да има странични ефекти, ако използваме $ constructor $), която също има всички функции от стабилната категория, което означава, че те са стабилни и в $ M [A] $. Може да забележите, че изрично въведохме друг клас функции като $ f: T to M [U] $. Например, $ pure $ и $ constructor $ са примери за такива функции за $ U = T $, но очевидно може да има и повече, като ако използваме $ pure $ и след това $ map $. И така, като цяло се нуждаем от начин за справяне с произволни функции под формата $ f: T to M [U] $.

Ако искаме да направим нова функция въз основа на $ f $, която може да се приложи към $ M [T] $, можем да опитаме да използваме $ map $. Но това ще ни накара да функционираме $ g: M [T] до M [M [U]] $, което не е добре, тъй като не искаме да имаме още една категория $ M [M [A]] $. За да се справим с този проблем, въвеждаме една последна функция: $ flatMap: (T to M [U]) to (M [T] to M [U]) $.

Но защо бихме искали да направим това? Да приемем, че сме след стъпка 2, т.е. имаме $ pure $, $ constructor $ и $ map $. Да кажем, че искаме да вземем HTML страница от toptal.com, след това да сканираме всички URL адреси там, както и да ги извлечем. Бих направил функция $ fetch: String to M [String] $, която извлича само един URL адрес и връща HTML страница.

Тогава щях да приложа тази функция към URL и да получа страница от toptal.com, което е $ x: M [String] $. Сега правя някаква трансформация на $ x $ и накрая стигам до някакъв URL $ u: M [String] $. Искам да приложа функция $ fetch $ към нея, но не мога, защото е от тип $ String $, а не $ M [String] $. Ето защо се нуждаем от $ flatMap $, за да преобразуваме $ fetch: String в M [String] $ в $ m_fetch: M [String] в M [String] $.

След като изпълнихме и трите стъпки, всъщност можем да съставим всякакви стойностни трансформации, от които се нуждаем. Например, ако имате стойност $ x $ от тип $ M [T] $ и $ f: T to U $, можете да използвате $ map $, за да приложите $ f $ за стойност $ x $ и да получите стойност $ y $ от тип $ M [U] $. По този начин всяка трансформация на стойности може да се извърши по 100 процента без грешки, стига изпълненията на $ pure $, $ constructor $, $ map $ и $ flatMap $ да не съдържат грешки.

Така че, вместо да се занимавате с някои гадни ефекти всеки път, когато ги срещнете във вашата кодова база, просто трябва да се уверите, че само тези четири функции са изпълнени правилно. В края на програмата ще получите само един $ M [X] $, където можете безопасно да разгънете стойността $ X $ и да се справите с всички случаи на грешки.

Това е монадата: нещо, което прилага $ pure $, $ map $ и $ flatMap $. (Всъщност $ map $ може да се извлече от $ pure $ и $ flatMap $, но това е много полезна и широко разпространена функция, така че не я пропуснах от дефиницията.)

Вариантната монада, известна още като Монадата може би

Добре, нека да се потопим в практическото прилагане и използване на монади. Първата наистина полезна монада е монадата Option. Ако идвате от класически езици за програмиране, вероятно сте срещали много сривове поради скандалната грешка с нулев указател. Тони Хоаре, изобретателят на null, нарича това изобретение „Грешката в милиарда долари“:

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

Така че нека се опитаме да подобрим това. Монадата Option или съдържа някаква ненулева стойност, или няма стойност. Доста подобно на нулева стойност, но разполагайки с тази монада, можем безопасно да използваме нашите добре дефинирани функции, без да се страхуваме от изключението на нулевия указател. Нека да разгледаме внедряванията на различни езици:

JavaScript - Опция Monad / Може би Monad

class Monad { // pure :: a -> M a pure = () => { throw 'pure method needs to be implemented' } // flatMap :: # M a -> (a -> M b) -> M b flatMap = (x) => { throw 'flatMap method needs to be implemented' } // map :: # M a -> (a -> b) -> M b map = f => this.flatMap(x => new this.pure(f(x))) } export class Option extends Monad { // pure :: a -> Option a pure = (value) => { if ((value === null) || (value === undefined)) { return none; } return new Some(value) } // flatMap :: # Option a -> (a -> Option b) -> Option b flatMap = f => this.constructor.name === 'None' ? none : f(this.value) // equals :: # M a -> M a -> boolean equals = (x) => this.toString() === x.toString() } class None extends Option { toString() { return 'None'; } } // Cached None class value export const none = new None() Option.pure = none.pure export class Some extends Option { constructor(value) { super(); this.value = value; } toString() { return `Some(${this.value})` } }

Python - Опция Monad / Може би Monad

class Monad: # pure :: a -> M a @staticmethod def pure(x): raise Exception('pure method needs to be implemented') # flat_map :: # M a -> (a -> M b) -> M b def flat_map(self, f): raise Exception('flat_map method needs to be implemented') # map :: # M a -> (a -> b) -> M b def map(self, f): return self.flat_map(lambda x: self.pure(f(x))) class Option(Monad): # pure :: a -> Option a @staticmethod def pure(x): return Some(x) # flat_map :: # Option a -> (a -> Option b) -> Option b def flat_map(self, f): if self.defined: return f(self.value) else: return nil class Some(Option): def __init__(self, value): self.value = value self.defined = True class Nil(Option): def __init__(self): self.value = None self.defined = False nil = Nil()

Рубин - Опция Монада / Може би Монада

class Monad # pure :: a -> M a def self.pure(x) raise StandardError('pure method needs to be implemented') end # pure :: a -> M a def pure(x) self.class.pure(x) end def flat_map(f) raise StandardError('flat_map method needs to be implemented') end # map :: # M a -> (a -> b) -> M b def map(f) flat_map(-> (x) { pure(f.call(x)) }) end end class Option Option a def self.pure(x) Some.new(x) end # pure :: a -> Option a def pure(x) Some.new(x) end # flat_map :: # Option a -> (a -> Option b) -> Option b def flat_map(f) if defined f.call(value) else $none end end end class Some