Частичными называют такие функции, которые не определены для некоторых значений своего аргумента. Например, отображение “рост_в_сантиметрах” из множества людей в множество натуральных чисел определено на всей области определения (каждому человеку поставлено в соответствие число), тогда как отображение “зарплата” определено не для каждого человека (кто-то не работает).

Компьютерная программа – это композиция (то есть функция от функции от … от функции аргумента) некоторых “функциональных блоков”. Например, программа-просмотрщик картинок вроде geeqie является удачной композицией функций из графической библиотеки GTK, библиотек декодирования изображений JPEG, PNG, стандартных библиотек языка Си и т.п. Программа geeqie могла бы быть написана следующим образом: a (b (c (f (g (h (имя_файла)))))) или, используя символ композиции, a . b . c . f . g . h имя_файла. Предполагается, что все функции определены для всех возможных аргументов (иначе программа содержит баги), поэтому композиция “функциональных блоков” имеет смысл и выполняется.

Как же нам быть, когда мы имеем дело не с полными функциями, а с частично определёнными? Ведь мы не можем в общем виде написать программу увеличения зарплаты, состоящую из композиции функциональных блоков “прибавка_к_зарплате” и “зарплата”, поскольку функция “зарплата” может быть определена не для всех людей. Конечно, в таком случае программисты добавят в блок “прибавка_к_зарплате” дополнительные условия и проверки, чтобы отловить момент когда из блока “зарплата” будет получена невалидная информация. Некоторые могут даже также использовать глобальные статические переменные. При длинной программе (композиции) проверять валидность данных придётся на всём пути их следования. Кроме того, в наши блоки придётся добавить функционал, смысл которого зачастую весьма далёк от семантики самого блока (например, зачем функции прибавке зарплаты знать о невалидных зарплатах?). Мы бы хотели отделить (а также не повторять код) обработку “невалидности” данных от непосредственной “обработки” этих данных.

Мы можем выразить частичные функции в самом общем смысле (для простоты я отойду от всех существующих конструкций Haskell):

-- область допустимых значений функции
data Partial a = Value a | NoMapping

-- тип частично определённой функции
type PartialF a b = (a -> Partial b)

Значением полиморфического типа Partial может быть Value a, если преобразование в тип a имело место, или NoMapping, если результат не может быть вычислен. Синонимом PartialF a b будем обозначать собственно частичную функцию из типа a в тип b. Вот, например, функция вычисления квадратного корня (как известно, не определённая на отрицательных числах):

safe_sqrt :: PartialF Double Double
safe_sqrt x | x < 0 = NoMapping
            | otherwise = Value (sqrt x)

А вот функции получения зарплаты (для тех, у кого она есть) и увеличения зарплаты:

data Person = John | Bob | Mary | Lisa | Alex

salary :: PartialF Person Double
salary John = Value 100
salary Bob = Value 70
salary Alex = Value 140
salary _ = NoMapping

increaseSalary :: PartialF Double Double
increaseSalary s = Value (increaseByTenPercent s)

increaseByTenPercent :: Double -> Double
increaseByTenPercent x = x + (0.1 * x)

Заметим, что функция increaseSalary совершенно справедливо ничего не знает о “неправильных” зарплатах. Она делает только что-то одно, а значит представляет собой нерушимый и независимый логический блок, решающий только одну подпроблему общей проблемы (программы). Способов увеличить зарплату может быть много, но здесь мы решили просто повысить её на 10 процентов. Функция повышения зарплаты не может знать как с помощью арифметики получить прибавку в 10 процентов, поэтому она вызывает полную функцию increaseByTenPercent, определённую для всех чисел Double. Почему мы решили, что increaseSalary – частично определённая функция? Потому что она представляет собой композициональный блок программы, в цепочке вызовов которой имеется частичная функция salary (а в цепочке композиций должны быть взаимосовместимые сигнатуры).

Кстати, а как нам описать композицию функций? Для обычных, полных функций это сделать легко (примерно так определена операция композиции . в Haskell):

compose :: (b -> c) -> (a -> b) -> a -> c
compose f g x = f (g x)

-- синоним
(.) = compose

Для композиции частичных функций нам понадобиться нечто с сигнатурой:

partial_compose :: PartialF b c -> PartialF a b -> PartialF a c
partial_compose f g x = undefined

Такая штука, очевидно, должна вычислить g x и, если отображение имело место, то есть было получено значение Value ret, произвести вычисления над результатом следующей функцией: f ret. Если первая функция не сможет вычислить результат для данного x, то выполнение второй функции не происходит, и вся операция композиции возвращает NoMapping.

Мы определим функцию >=> (порядок композиции для удобства изменим на обратный):

(>=>) :: PartialF a b -> PartialF b c -> PartialF a c
(>=>) m1 m2 = \a -> let
  pb = m1 a
  pc = case pb of
    NoMapping -> NoMapping
    Value b -> m2 b
  in
    pc

Вот как работает наша композиция на примере программы, которая дважды увеличивает зарплату сотруднику John:

> (salary >=> increaseSalary >=> increaseSalary) John
Value 121.0

> (salary >=> increaseSalary >=> increaseSalary) Mary
NoMapping

> let double_inc = increaseSalary >=> increaseSalary
> (salary >=> double_inc >=> safe_sqrt) John
Value 11.0

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