Главная » C# » Чистые функции – функциональное программирование

0

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

Использование методов объектно-ориентированного программирования подразевает  написание  кода с  побочными  эффектами.  Это  принимается  как  должное, т. к., применяя методы объектно-ориентированного программирования, разрабоик должен скрывать реализацию и не открывать состояние объекта.

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

Рассмотрим следующую операцию сложения:’

2 + 2   =   4

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

class ClassWithSideEffects { public ClassWithSideEffects() {

_islnitialized = false;

}

bool _islnitialized; string _value;

public void Initialize(string value) {

_islnitialized = true;

_value = value;

}

public string GetMeAValue() { if (_islnitialized) {

return _value;

}

throw new NullReferenceException("Not initialized");

}

}

Клиент,   вызывающий   метод  GetMeAValue о,   имеет  проблему,   которая   состоит в том, что он  получит два разных ответа,  или два разных будущих. Первым будим является действительный ответ, а вторым будущим — исключение. Какое бущее клиент получит, находится вне его контроля, что является стохастическим поведением. Данный код является  примером программирования  с  побочными эектами,  т. к.  для  того  чтобы  код работал,  необходимо  предварительно  вызвать

метод initialize о. Но клиент метода GetMeAValue()  не  знает,  что  нужно  вызать метод initialize (), т.  к. у него нет указания делать это.

Простыми словами, код без побочных эффектов представляет собой функцию,  корая получает все свои данные посредством набора параметров и возвращает мификации вызывающему клиенту. Сама функция не сохраняет никаких модифаций.  Рассмотрим,  например,  следующий  код:

(vail, val2) => vail + val2

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

(val) => { val.DoSomethingO; return val.GetMeAValueO; }

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

Использование неизменяемых типов

Для написания императивного кода, свободного от  побочных эффектов, можно применять неизменяемые объекты (immutable objects). В действительности, мы иользовали один неизменяемый тип во всей этой  книге.  Можете  сказать,  какой это тип? Строковый тип. Значение, присвоенное строковому типу, нельзя изменить. Неизменяемые типы решают многие проблемы, включая проблемы параллелизма, непротиворечивости и побочных эффектов. Но неизменяемые типы имеют и недоаток, состоящий в том, что для них требуется больше ресурсов и, в зависимости от кода,  они  могут замедлить  производительность.

ПРИМЕЧАНИЕ

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

Рассмотрим модифицированный код класса  ciasswithsideEffects, чтобы  сделать его  неизменяемым:

class ClassWithNoSideEffects { private readonly string _value;

public ClassWithNoSideEffects(string value) {

_value = value;

}

public string GetMeAValue() { return _value;

}

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

class MyClass {

readonly int value = 10;

Класс classwithNosideEffects не  имеет никаких побочных эффектов, т. к. метод GetMeAValue () всегда возвращает один и тот же результат: значение, присвоенное классу при его создании конструктором. В инициализации класса нет надобности, т. к.  его  экземпляр  инициализируется  при  создании.  Совмещение  инициализации с созданием экземпляра класса означает, что всегда, когда нам нужен объект, мы инициализируем его.

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

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

class SalesTax {

private readonly double _percentage;

public SalesTax(double percentage) {

_percentage = percentage;

}

public double Percentage { get {

return percentage;

}

public double CalculateGrandTotal(double itemTotal) { return itemTotal + itemTotal * percentage;

}

Класс SalesTax имеет один член данных, percentage, который представляет раер налога в процентах, налагаемый на стоимость  приобретенного  товара.  Этот член данных объявлен readonly, таким образом, обеспечивая, что ему можно првоить значение лишь в конструкторе, после чего он остается неизменяемым. Раер налога считывается С ПОМОЩЬЮ свойства Percentage.

Общая сумма, включающая налог с продаж, вычисляется методом CalculateGrandTotal (), который является чистой функцией, т.к . он всегда воращает один  и тот же результат.  Так как член данных percentag e модифицирован ключевым словом readonly, ТО когда МЫ доходим ДО метода CalculateGrandTotal (), ему присваивается значение, которое никогда не изменяется.

Манипулирование неизменяемыми типами

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

Манипулирование неизменяемым объектом посредством метода типа означает соание метода, который создает и возвращает новый экземпляр типа, где метод обвлен в самом типе. Этот подход применяется для манипулирования строковым типом. В случае класса SalesTax это означает переопределение типа следующим образом:

class SalesTaxWithMethod {

private readonly double percentage;

public SalesTaxWithMethod(double percentage) { percentage = percentage;

}

public double Percentage { get {

return percentage;

}

}

public double CalculateGrandTotal(double itemTotal) { return itemTotal + itemTotal * _percentage;

}

public SalesTaxWithMethod AddPercentage(double percentage) { return new SalesTaxWithMethod(percentage + .percentage);

}

}

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

ПРИМЕЧАНИЕ

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

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

•    это легкий подход, не требующий  изучения  новых  программных  конструктов С#;

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

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

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

(salesTax, percentage) => new SalesTax(percentage + salesTax.Percentage)

Лямбда-выражения  являются  самым  простым  и  наименее  инвазивным  подходом к манипулированию неизменяемыми объектами. При использовании данного подхода изменять базовый тип нет надобности. Но это также наиболее неудобный

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

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

Работая с методами расширения, мы определяем внешнее выражение, но объявлие метода расширения делает метод таким, как будто бы он был объявлен в типе. Для класса SalesTax метод расширения был бы объявлен следующим образом:

static class ClassExtension {

public static SalesTax AddPercentage(this SalesTax els,

double percentage) { return new SalesTax(els.Percentage + percentage);

}

}

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

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

Источник: Гросс  К. С# 2008:  Пер. с англ. — СПб.:  БХВ-Петербург, 2009. — 576 е.:  ил. — (Самоучитель)

По теме:

  • Комментарии