2015-08-19 2 views
1

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

Предположим, у меня есть этот код

f1 a b c d = e 
where 
    e1 = f2 b c (f3 a) 
    e2 = f4 d 
    e = e1 + e2 

f2 b c d = n + c + d 
where 
    n = f5 b 

f5 n = n*n 

f3 a = a * 2 

f4 a = a + 3 

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

f1 a b c d x = e -- f1 needs to be changed 
where 
    e1 = f2 b c (f3 a) x 
    e2 = f4 d 
    e = e1 + e2 

f2 b c d x = n + c + d -- f2 needs to be changed 
where 
    n = f5 b x 

f5 n x = n*n -- f5 changed (**bang**) 

f3 a = a * 2 

f4 a = a + 3 

Это нормальный Haskell способ делать такого рода вещи, или есть лучше (Haskell-МОГ) способ? Я знаю, что такое изменение в API будет нарушать клиентский код, но как влияние может быть минимальным и есть ли какой-либо способ Hasekll для этого?

На более общем уровне: насколько хорошо Haskell выполняет в таких случаях (особенно принимая во внимание его неизменяемое состояние)? Что предложить разработчикам в этом отношении? Или это то, что Haskell не играет никакой роли, по сути, играть в этом и что это просто сложная проблема разработки программного обеспечения (нет такой вещи, как будущего доказательства), что нам нужно идти в ногу с этим?

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

+3

Ну, вы всегда можете изменить подпись типа и список аргументов (вы пишете сигнатуры типов, не так ли?), А затем смотрите, где компилятор жалуется. Исправьте первое место, затем вернитесь к шагу 2 до тех пор, пока не будут исчерпаны все ошибки. Если у вас есть общий набор аргументов, которые передаются многим функциям, вероятно, было бы лучше сделать новый тип данных для хранения всех этих значений, тогда вы можете избежать изменения аргументов функции и вместо этого просто изменить определение этого типа. Это проблема программного обеспечения, а не Haskell. – bheklilr

ответ

0

Одна вещь, которую вы можете сделать, это расслоение параметров, подобных этому, в объект одного параметра, как предлагает bhelkir в комментарии. Если вы добавите новый параметр к этому объекту, вам все равно придется изменить код клиента, который вызывает f1, и изменить прямых потребителей этого нового параметра (здесь f5), но это неизбежно: кто-то должен предоставить x на некоторых точка, и вам нужно, чтобы он был клиентом; и вы должны потреблять x как-то иначе, почему вы добавляете его для начала?

Но вы можете избежать изменения посреднических функций, таких как f1 и f2, потому что они могут игнорировать новые поля, которые им не нужны. И вы можете получить немного фантазии, используя экземпляр Applicative для ((->) t) (обычно называемый Reader), чтобы пройти по этому объекту, а не выполнять его вручную. Вот один из способов, чтобы написать, что:

module Test where 
import Control.Applicative 

data Settings = Settings {getA :: Int, 
          getB :: Int, 
          getC :: Int, 
          getD :: Int} 
f1 :: Settings -> Int 
f1 = liftA2 (+) f2 f4 
-- f1 = do 
-- e1 <- f2 
-- e2 <- f4 
-- return $ e1 + e2 

f2 :: Settings -> Int 
-- probably something clever with liftA3 and (+) is possible here too 
f2 s = f5 s + getC s + f3 s 

f3 :: Settings -> Int 
f3 = liftA (* 2) getA 

f4 :: Settings -> Int 
f4 = liftA (+ 3) getD 

f5 :: Settings -> Int 
-- f5 = liftA (join (*)) getB -- perhaps a bit opaque 
f5 = liftA square getB 
    where square b = b * b 

Теперь, в этом есть свои плюсы и свои минусы: логика, которая была в f1 (то есть, зная, что вам нужно позвонить f3 с a) переместилась в самой f3, и это произойдет для любой функции, которая изначально считывала параметр и удаляла его, прежде чем передавать его на некоторую вспомогательную функцию. Это может быть яснее оригинала или может затенять намерение позади f1, в зависимости от вашего проблемного домена. Вы всегда можете написать функцию более явно, например, изменив переданный объект Settings, чтобы изменить его поле a, прежде чем передавать его, как это было в случае с f2. В более общем плане вы можете написать любую функцию в зависимости от того, какой стиль наиболее удобен для нее: do-notation, аппликативные функции или простое сопоставление образцов по объекту записи.

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

module Test where 
import Control.Applicative 

data Settings = Settings {getA :: Int, 
          getB :: Int, 
          getC :: Int, 
          getD :: Int, 
          getX :: Int} 
f1 :: Settings -> Int 
f1 = liftA2 (+) f2 f4 
-- f1 = do 
-- e1 <- f2 
-- e2 <- f4 
-- return $ e1 + e2 

f2 :: Settings -> Int 
-- probably something clever with liftA3 and (+) is possible here too 
f2 s = f5 s + getC s + f3 s 

f3 :: Settings -> Int 
f3 = liftA (* 2) getA 

f4 :: Settings -> Int 
f4 = liftA (+ 3) getD 

f5 :: Settings -> Int 
f5 = liftA2 squareAdd getB getX 
    where squareAdd b x = b * b + x 

Обратите внимание, что все это то же самое, за исключением data Settings и f5.

+0

Умеет разбираться в том, что он хорошо работает с дефолтами по отношению к будущим изменениям API. Поэтому, если у вас есть 'defaultSettings', вы можете заставить пользователя выполнить' defaultSettings {getC = 3} ', чтобы изменить' getC'. Самое приятное то, что если вы решили добавить, скажем, поле 'getE', код' defaultSettings {getC = 3} еще работает :) –

Смежные вопросы