https://pragprog.com/titles/swdddf/domain-modeling-made-functional/
해당 시리즈는 Domain Modeling Made Functional 책의 번역 및 정리 내용입니다.
Functions, Functions, Everywhere
먼저 함수형 프로그래밍이 객체 지향 프로그래밍과 왜 다른지 살펴보겠습니다.
- 함수형 프로그래밍은 함수를 가장 중요한 것으로 다루며 프로그래밍하는 것입니다.
- 객체 지향 접근 방식에서 이러한 조각은 클래스와 객체가 됩니다.
- 함수형 접근 방식에서 이러한 부분은 함수가 됩니다.
- 객체 지향 접근 방식에서는 상속이나 Decorator 패턴과 같은 기술을 사용할 수 있습니다.
- 함수형 프로그래밍은 재사용 가능한 모든 코드를 함수에 넣고 합성을 사용하여 함께 붙입니다.
Functions Are Things (함수는 값이다.)
고차 함수 :
F#에서 함수를 값으로 취급하기
let plus3 x = x + 3 // plus3 : x:int -> int
let times2 x = x * 2 // times2 : x:int -> int
let square = (fun x -> x * x) // square : x:int -> int
let addThree = plus3 // addThree : (int -> int)
// listOfFunctions : (int -> int) list
let listOfFunctions =
[addThree; times2; square]
F#에서 리스트 리터럴은 대괄호를 구분 기호로 사용하고 세미콜론(쉼표 아님!)을 요소 구분 기호로 사용합니다.
for fn in listOfFunctions do
let result = fn 100 // call the function
printfn "If 100 is the input, the output is %i" result
// Result =>
// If 100 is the input, the output is 103
// If 100 is the input, the output is 200
// If 100 is the input, the output is 10000
// myString : string
let myString = "hello"
// square : x:int -> int
let square x = x * x
// square : x:int -> int
let square = (fun x -> x * x)
입력으로서의 함수
let evalWith5ThenAdd2 fn =
fn(5) + 2
// evalWith5ThenAdd2 : fn:(int -> int) -> int
let add1 x = x + 1 // an int -> int function
evalWith5ThenAdd2 add1 // fn(5) + 2 becomes add1(5) + 2
// // so output is 8
let square x = x * x // an int -> int function
evalWith5ThenAdd2 square // fn(5) + 2 becomes square(5) + 2
// // so output is 27
출력으로서 함수
let add1 x = x + 1
let add2 x = x + 2
let add3 x = x + 3
let adderGenerator numberToAdd =
// return a lambda
fun x -> numberToAdd + x
// val adderGenerator :
// int -> (int -> int)
let adderGenerator numberToAdd =
// define a nested inner function
let innerFn x =
numberToAdd + x
// return the inner function
innerFn
// test
let add1 = adderGenerator 1
add1 2 // result => 3
let add100 = adderGenerator 100
add100 2 // result => 102
Currying
// int -> int -> int
let add x y = x + y
// int -> (int -> int)
let adderGenerator x = fun y -> x + y
Partial Application
// sayGreeting: string -> string -> unit
let sayGreeting greeting name =
printfn "%s %s" greeting name
// sayHello: string -> unit
let sayHello = sayGreeting "Hello"
// sayGoodbye: string -> unit
let sayGoodbye = sayGreeting "Goodbye"
sayHello "Alex"
// output: "Hello Alex"
sayGoodbye "Alex"
// output: "Goodbye Alex"
Total Functions
let twelveDividedBy n =
match n with
| 6 -> 2
| 5 -> 2
| 4 -> 3
| 3 -> 4
| 2 -> 6
| 1 -> 12
| 0 -> ???
let twelveDividedBy n =
match n with
| 6 -> 2
...
| 0 -> failwith "Can't divide by zero"
(예외는 타입 시그니처에서 숨어버립니다)
twelveDividedBy : int -> int
한 가지 방법은 입력을 제한하여 잘못된 값을 제거하는 것입니다.
type NonZeroInteger =
// Defined to be constrained to non-zero ints.
// Add smart constructor, etc
private NonZeroInteger of int
/// Uses restricted input
let twelveDividedBy (NonZeroInteger n) =
match n with
| 6 -> 2
...
// 0 can't be in the input
// so doesn't need to be handled
twelveDividedBy : NonZeroInteger -> int
또 다른 기술은 아웃풋 타입을 확장하는 것입니다.
/// Uses extended output
let twelveDividedBy n =
match n with
| 6 -> Some 2 // valid
| 5 -> Some 2 // valid
| 4 -> Some 3 // valid
...
| 0 -> None // undefined
twelveDividedBy : int -> int option
유효한 int를 입력하면 int를 줄 수도 있다는 것을 의미합니다.
Composition
함께 합성한 후에는 새로운 함수가 있습니다.
F#에서 함수의 합성
F#에서 함수 합성은 어떻게 동작합니까?
F#에서 첫 번째 함수의 출력 형식이 두 번째 함수의 입력 형식과 같기만 하면 두 함수를 함께 사용할 수 있습니다.
이것은 일반적으로 "piping"이라는 접근 방식을 사용하여 수행됩니다.
let add1 x = x + 1 // an int -> int function
let square x = x * x // an int -> int function
let add1ThenSquare x =
x |> add1 |> square
// test
add1ThenSquare 5 // result is 36
add1ThenSquare에 대한 파라미터 x를 정의했습니다.
구현에서 해당 매개변수는 파이프라인을 통한 데이터 흐름을 시작하기 위해 첫 번째 함수(추가)에 제공됩니다.
다른 예가 있습니다. 첫 번째 함수는 int->bool 함수이고 두 번째 함수는 bool->string 함수이며 결합된 함수는 int->string입니다.
let isEven x =
(x % 2) = 0 // an int -> bool function
let printBool x =
sprintf "value is %b" x // a bool -> string function
let isEvenThenPrint x =
x |> isEven |> printBool
// test
isEvenThenPrint 2 // result is "value is true"
js의 const isEvenThenPrint = (x)=>(isEven(printBool(x));와 같습니다.
전체 애플리케이션을 함수로 빌드
함수 합성 법칙은 완전한 애플리케이션을 구축하는 데 사용할 수 있습니다.
예를 들어 애플리케이션의 저수준에서 기본 기능으로 시작합니다.
다음 서비스 함수를 만들기 위해 저수준 오퍼레이션을 합성합니다.
그런 다음 이러한 서비스 함수를 사용 및 결합하여 전체 워크플로를 처리하는 함수를 만들 수 있습니다.
복습 : 워크플로는 1인 혹은 팀이 수행할수 있는 업무 단위입니다.
함수 합성의 과제 (모나드)
1번 예제의 첫번째 함수는 Option<int>를 출력하지만 두 번째 함수는 입력으로 primitive int가 필요합니다.
2번 함수의 첫 함수는 primitive int를 출력하지만 두 번째 함수는 입력으로 Option<int>이 필요합니다.
lists, the success/failure Result type, async 등 Wrapper type 에서 발생합니다.
해당 예제 코드입니다.
// a function that has an int as output
let add1 x = x + 1
// a function that has an Option<int> as input
let printOption x =
match x with
| Some i -> printfn "The int is %i" i
| None -> printfn "No value"
Some 생성자를 사용하여 add1의 출력을 Option으로 변환한 다음 printOption의 입력으로 파이프할 수 있습니다.
5 |> add1 |> Some |> printOption
추가로 읽어보기 : 책에서 다루지 않는 예제 코드
아래 페이지에서 직접 돌려보는것도 도움이 됩니다.
// 문법 리뷰
module Syntax =
//>SyntaxValues
// single line comments use a double slash
// 더블 슬래시는 주석
// The "let" keyword defines an (immutable) value
// let 키워드는 불변 변수 선언
let myInt = 5
let myFloat = 3.14
let myString = "hello" // note that no types needed
//<
// ======== Lists ============
//>lists
// 리스트는 ; 딜리미터
let twoToFive = [2;3;4;5] // Square brackets create a list with
// semicolon delimiters.
//let twoToFive = 2::3::4::[5]
// ::는 하스켈의 cons 오퍼레이터. [1;2;3;4;5]는 해당 문법의 문법적 설탕에 불과함
let oneToFive = 1 :: twoToFive // :: creates list with new 1st element
// The result is [1;2;3;4;5]
// @는 concat Operator. 대부분 함수형 언어에서는 오퍼레이션이 infix와 prefix 전부 지원.
let zeroToFive = [0;1] @ twoToFive // @ concats two lists
// IMPORTANT: commas are never used as delimiters, only semicolons!
//<
// ======== Functions ========
//>SyntaxFunctions
// The "let" keyword also defines a named function.
// let을 통해 함수 선언 가능
let square x = x * x // Note that no parens are used.
// In F# returns are implicit -- no "return" needed. A function always
// returns the result of the last expression.
// F#에는 return 키워드가 없음. 리턴이 암시적임. 마지막 표현식의 평가 결과를 리턴함.
let add x y = x + y // don't use add (x,y)! It means something
// completely different.
// 함수 호출 예제.
square 3 // Run the function. No parens!
add 2 3 // Run the function. No parens!
// to define a multiline function, just use indents. No semicolons needed.
// 멀티라인 표현식에도 ; 필요 없음
let evens list =
let isEven x = x%2 = 0 // Define "isEven" as an inner ("nested") function
List.filter isEven list // List.filter is a library function
// with two parameters: a boolean function
// and a list to work on
evens oneToFive // Now run the function
//<
//>SyntaxRecords
// Record types have named fields. Semicolons are separators.
// 레코드 문법. 타입 선언
type Person = {First:string; Last:string}
// To create a value of a record type, use similar syntax to the definition
// and assign each field to a value
// 레코드 문법. 값 생성
let person1 = {First="john"; Last="Doe"}
//<
//>SyntaxUnions
// Union types have choices. Vertical bars are separators.
// 초이스 타입. 왼쪽이 타입 오른쪽은 Case 생성자.
type Temp =
| DegreesC of float
| DegreesF of int
// To create a value of a union type, use the case tag as the constructor
// 타입 Temp는 Case를 통해 Wrapper 타입을 생성함.
// 내부적으로는 배열처럼 처리하거나, 리스트 객체처럼 처리함.
// 사용자는 Wrapping, unWrapping만 하면 됨.
let tempInC = DegreesC 37.1
let tempInF = DegreesF 98
//<
//>SyntaxComplex
// Types can be combined recursively in complex ways.
// E.g. here is a union type that contains a list of the same type:
type Employee =
| Worker of Person
| Manager of Employee list
let jdoe = {First="John";Last="Doe"}
let worker = Worker jdoe
//<
// ========= Printing =========
//>SyntaxPrinting
// The printf/printfn functions are similar to the
// Console.Write/WriteLine functions in C#.
printfn "Printing an int %i, a float %f, a bool %b" 1 2.0 true
printfn "A string %s, and something generic %A" "hello" [1;2;3;4]
// all complex types have pretty printing built in (using %A)
// 알아서 Wrapper type 안의 값을 까서 프린트해줌.
// Person={First = john;Last = Doe},Temp=DegreesC 37.1,Employee=Worker {First = John; Last = Doe}
printfn "Person=%A,\nTemp=%A,\nEmployee=%A"
person1 tempInC worker
//<
// ======== Pattern Matching ========
// Match..with.. is a supercharged case/switch statement.
let simplePatternMatch =
let x = "a"
match x with
| "a" -> printfn "x is a"
| "b" -> printfn "x is b"
| _ -> printfn "x is something else" // underscore matches anything
// Some(..) and None are roughly analogous to Nullable wrappers
let validValue = Some(99)
let invalidValue = None
// In this example, match..with matches the "Some" and the "None",
// and also unpacks the value in the "Some" at the same time.
let optionPatternMatch input =
match input with
| Some i -> printfn "input is an int=%d" i
| None -> printfn "input is missing"
optionPatternMatch validValue
optionPatternMatch invalidValue
'BackEnd' 카테고리의 다른 글
함수 파이프라인 합성하기 with F# 2. 작은 파이프라인 조합하기 (0) | 2022.03.20 |
---|---|
함수 파이프라인 합성하기 with F# 1. 검증 로직 구현하기 (0) | 2022.03.19 |
파이프라인으로 워크플로 모델링하기 - 워크플로 합성 및 나머지 (0) | 2022.03.19 |
파이프라인으로 워크플로 모델링하기 - 함수 타입으로 워크플로 모델링, 이펙트 모델링 (0) | 2022.03.19 |
파이프라인으로 워크플로 모델링하기 - 개요 및 상태 머신 (0) | 2022.03.19 |