2022.01.23
함수형 언어에서의 코드 재사용은 객체지향 언어와는 접근 방법이 다르다. 객체지향 언어는 클래스에 종속된 메서드를 만드는 것을 권장하여 반복되는 패턴을 재사용하려 한다.
함수형 언어는 자료구조에 대해 공통된 변형 연산을 적용하고, 특정 경우에 맞춰서 주어진 함수를 사용하여 작업을 커스터마이즈함으로써 재사용을 장려한다.
언어들이 소프트웨어에서 반복되는 문제들의 해결 방법을 어떻게 진화시켜왔는지를 살펴보자.
100개의 함수를 하나의 자료구조에 적용하는 것이 10개의 함수를 10개의 자료구조에 적용 하는 것보다 낫다.
- 앨런 펄리스 Alan Perlis -
여기서 디스패치란 넓은 의미로 언어가 작동 방식을 동적으로 선택하는 것을 말한다.
class LetterGrade {
def gradeFromScore(score) {
switch(score) {
case 90..100 : return "A"
case 80..<90 : return "B"
...
case ~"[ABCDFabcdf]" : return score.toUpperCase()
}
}
}
(defn in [score low high]
(and (number? score) (<= low score high)))
(defn letter-grade [score]
(cond
(in score 90 100) "A"
(in score 80 90) "A"
...
(in score 0 60) "F"
(re-find #"[ABCDFabcdf]" score) (.toUpperCase() score)))
(deffn basic-colors-in [color]
(for [[k v] color :when (not= v 0)] k))
(defmulti color-string basic-color-in)
(defmulti color-string [:red] [color]
(str "Red: " (:red color)))
(defmulti color-string [:green] [color]
(str "Green: " (:green color)))
(defmulti color-string [:blue] [color]
(str "Blue: " (:blue color)))
(defmulti color-string [:blue] [color]
(str "Blue: " (:blue color)))
함수형 언어의 공통적인 기능은 연산자 오버로딩이다. +,-,*와 같은 연산자를 새로 정의하여 새로운 자료형에 적용하고 새로운 행동을 하게 하는 기능이다.
새로운 언어를 만들지 말고, 연산자 오버로딩을 통해 문제 도메인을 향하여 언어를 구부리자.
예외는 많은 함수형 언어가 준수하는 전제 몇 가지를 깨트린다.
함수형 언어는 부수효과가 없는 순수함수를 선호한다.
함수형 프로그램이 선호하는 또 하나의 특성은 참조 투명성이다.
자바에서 예외를 사용하지 않고 오류를 처리하기 위해 해결해야 할 근본적인 문제는 메서드가 하나의 값만 리턴할 수 있다는 제약이다.
public static Map<String, Object> divide(int x, int y) {
Map<String, Object> result = new HashMap<String, Object>();
if (y == 0)
result.put("exception", new Exception("div by zero"));
else
result.put("answer", (double) x / y);
return result;
}
이 접근 방법에는 문제점이 있다.
여기서 필요한 것은 타입 세이프하게 둘 또는 더 많은 값을 리턴할 수 있게 해주는 메커니즘이다.
type Error = String
type Sucess = String
def call(url:String):Either[Error,Success]={
val response = WS.url(url).get.value.get
if(valid(response))
Right(response.body)
else Left("Invalid Response")
}
Either를 사용하면 타입 세이프티를 유지하면서 예외 또는 제대로 된 결과 값을 리턴하는 코드를 만들 수 있다. 함수형의 보편적인 관례에 따라 Either 클래스의 왼쪽이 예외, 오른쪽이 결과 값이다.
여러 언어나 프레임워크에는 Either와 유사한 Option이란 클래스가 있다. 이것은 적당한 값이 존재하지 않을 경우를 의미하는 none, 성공적인 리턴을 의미하는 some을 사용하여 예외 조건을 더 쉽게 표현한다.
public static Option<Double> divide<double x, double y) {
if (y == 0)
return Option.none();
return Option.some(x / y);
}
Option은 Either의 left와 right과 유사하지만 적당한 리턴 값이 없을 수 있는 메서드를 위해 none()과 some()을 가지고 있다. Either는 어떤 값이든 저장할 수 있는 반면, Option은 주로 성공과 실패의 값을 저장하는 데 쓰인다.
Either 같은 자료구조의 이점을 알기 위해서는 패턴 매칭에 대해 다뤄야 한다.
// 패턴 매칭과 유사한 구문으로 트리의 깊이 알아내기
static public int depth(Tree t) {
for (Empty e : to.toEither().left())
return 0;
for (Either<Leaf, Node> ln: to.toEither().right()) {
for (Leaf leaf : ln.left())
return 1;
for (Node node : ln.right())
return 1 + max(depth(node.left), depth(node.right));
}
throw new RuntimeException("Inexhaustible pattern match on tree");
}
depth() 메서드는 깊이를 알아내는 재귀 함수이다. 트리가 <Either, <Left, Node>>
란 특정한 자료구조로 만들어진 것이기 때문에, 각각의 '슬롯'을 특정한 경우로 생각할 수 있다.
트리의 내부 구조를 규격화한 덕분에, 트리를 따라가면서 각 요소의 자료형에 따른 경우에 대해서만 생각하며 분석할 수 있다.