FP in Elm 노트 – Intro to FRP in Elm

😢 이 페이지는 다음 주소로 변경될 예정입니다.

FP in Elm의 week 1-2 Intro to FRP in Elm 정리 포스트다.


Introduction to FRP in Elm

JS 이벤트 리스너 코드 예제를 보여주면서 IsDown같은 변수를 만들어 상태를 저장해야 하는 부분, 콜백으로 지정하는 복잡성 등에 얘기하며 FRP에서는 더 쉽게(?) 할 수 있다고 이야기.

Signal이란

“시그널은 시간에 따라 변동되는 값” 원시적 시그널은 Signal Bool을 반환.

함수형 프로그래밍 블럭을 만들어서 시그널을 추상화하거나 합성할 수 있음.

Signal.map : (a -> b) -> Signal a -> Signal b
isUp = Signal.map (fun curValIsDown -> not curValIsDown) Mouse.isDown
-- or,
isUp = Signal.map not Mouse.isDown

이제 Mouse.isDown 시그널이 업데이트 되면 자동으로 반영됨.

Signal.map과 같이 함수를 각각에 맵핑하는 것은 다른 데이터에서도 많이 사용. List.map, String.map, Maybe.map, Dict.map, etc.

HTML로 렌더링하기

main에 정의하는 것으로 HTML을 렌더링 할 수 있음. Graphics.Element

elm-repl을 사용할 수 없으므로 elm-make, 온라인 에디터를 활용, 또는 로컬 환경을 활용.

Text.plainText는 최근 버전에서 삭제되서 구버전을 설치하거나 changelog를 참고해야 한다. 아래는 [Text](http://package.elm-lang.org/packages/elm-lang/core/2.1.0/Text) 를 참고했다.

import Text exposing (fromString)
import Graphics.Element (Element, leftAligned)

plainText = leftAligned << fromString

main : Element
main = plainText "Hello World!"

Text.fromStringStringText로 변환하고 Graphics.Element.leftAlignedTextElement로 변환해준다. 코드에서 필요한 함수를 import 한 것도 확인할 수 있다.

실제로 mainSignalElement로 정의되어 있다. 위와 같이 다른 Element는 Signal.constant를 이용해 Signal로 변환된다.

Signal.constant : a -> Signal a

위 예제를 더 명시적으로 작성하면 다음과 같다.

main : Signal Element
main = Signal.constant(plainText "Hello World!")

위 내용으로 작성한 예제.

import Text exposing (fromString)
import Graphics.Element exposing (Element, leftAligned)
import Mouse
import Signal

plainText = leftAligned << fromString
isUp = Signal.map not Mouse.isDown

main : Signal Element
main = Signal.map (\b -> plainText(toString b)) isUp

import에 대해

매번 쓸 때마다 앞 코드처럼 map하지 말고 다른 모듈로 만들어서 재활용하라는 이야기.

함수 합성에 대해

다음은 함수를 합성하는 여러 방법. 취향에 맞게 선택을 하라는데 가장 마지막 방식이 많이 쓰는 모양.

(\b -> b |> toString |> plainText)
(toString >> plainText)
(\b -> plainText <| toString <| b)
(plainText << toString)

Folding From the Past

클릭 횟수를 보여주는 페이지를 만들려고 함. 이런 signal은 내장되어 있지 않은데 다음과 같이 모조 유닛 ()으로 된 이벤트를 정의할 수 있음.

Mouse.clicks : Signal ()

기본 MVC 아키텍쳐

-- Model
type alias State = Int

initState : State
initState = 0

alias는 새 타입 정의 없이 Int와 동일한 역할을 하는 State를 만들어줌. abbr 만들 때도 쓸 수 있고.

-- View
view : State -> Element
view s = asText s
-- 더 간결하게
view = asText

asTextTextasText에서 Graphics.Elementshow변경됨.

컨트롤러는 시그널이 갱신되었을 때, 상태를 변형하거나 렌더링하는 함수를 시그널과 연결해주는 역할을 한다. 타입 a는 전체 값, 타입 b는 초기값, 마지막 타입 b는 최종 상태값이 된다.

Signal.foldp : (a -> b -> b) -> b -> Signal a -> Signal b

List.foldl : (a -> b -> b) -> b -> List a -> b
List.foldr : (a -> b -> b) -> b -> List a -> b

folding from the past 는 from the left라는 말이고 from the future는 from the right이 된다. 시그널은 folding from the past로 호출한다.

step : a -> State -> State
step _ i = i + 1

main : Signal Element
main =
  Signal.map view (Signal.foldp step initState Mouse.clicks)

다른 예제는 여기.

main = Signal.map VIEW (Signal.foldp UPDATE INIT_STATE BASE_SIGNAL)
-- so,
main = Signal.map view (Signal.foldp step initState (Time.every Time.second))

잠깐만

맨 처음 예로 든 JS와 달리 부수적인 부분은 다 컴파일러가 알아서 함.

JavaScript로의 컴파일링

원초적인 이벤트는 앞서 구현한 JS에서와 같이 다뤄짐. 하지만 elm이 Signal Graph로 처리해 순수 함수 형태로 정의된 Signal을 사용할 수 있게 함.

각각의 기능이 순수한 함수 형태로 다른 함수에 영향을 주지 않아 전체를 컴파일 할 필요가 없어짐. 이런 접근 방식은 함수형 언어 컴파일러 최적화와 관련되어 중요한 개념 중 하나.

2D 그래픽

Graphics.Element, Graphics.Collage 공부할 것.

읽을 거리

필수

  • 라이브러리 Signal, Graphics.Element, Graphics.Collage

Signal에 정의된 (<~)는 위에서 정의한 isUp을 간편하게 정의하는데 유용함.

isUp = not <~ Mouse.isDown
-- `Mouse.isDown` 시그널을 `not` 함수를 통해 전달

추천

심화

FP in Elm 노트 – Intro to ML in Elm

😢 이 페이지는 다음 주소로 변경될 예정입니다.

FP in Elm의 week 1-1-2 Intro to ML in Elm 정리 포스트다.


Introduction to ML in Elm

Elm은 웹사이트에서 받아 설치한다. REPL로 진행한다.

$ elm-repl
Elm REPL 0.4.2 (Elm Platform 0.15.1)
  See usage examples at <https://github.com/elm-lang/elm-repl>
  Type :help for help, :exit to exit
> True
True : Bool
> False
False : Bool
> 'a'
'a' : Char
> "abc"
"abc" : String
> 3.0
3 : Float

numberIntFloat 모두를 의미. 타입 검사기가 알아서 선택.

> 3
3 : number
> truncate 3
3 : Int
> truncate 3.0
3 : Int

number가 하스켈에서 type 클래스로 미리 정의한 것처럼 보이지만 Elm엔 일반적으로 타입 클래스 지원이 없음. 이 number는 몇가지 특별한 용도로 사용할 수 있는 클래스 중 하나.

Tuples

두번째 튜플에서 ‘가 여러개 붙는 이유는 모르겠다. 타입이 달라질 수 있어서 그런 것 같기도. 튜플에 컴포넌트가 하나면 튜플이 아닌 것으로 취급.

> (True, False)
(True,False) : ( Bool, Bool )
> (1,2,3,4.0)
(1,2,3,4) : ( number, number', number'', Float )

> ("Leave me alone!")
"Leave me alone!" : String
> (("Leave me alone!"))
"Leave me alone!" : String

Functions

인자 하나, 반환값 하나를 갖는 함수:

> exclaim = \s -> s ++ "!"
<function> : String -> String
> exclaim s = s ++ "!"
<function> : String -> String
> exclaim "HI"
"HI!" : String

uncurried/curried 스타일 다인자 함수:

> plus (x, y) = x + y
<function> : ( number, number ) -> number
> plus = \(x, y) -> x + y
<function> : ( number, number ) -> number
> plus xy = fst xy + snd xy
<function> : ( number, number ) -> number

> plus x y = x + y
<function> : number -> number -> number
> plus x = \y -> x + y
<function> : number -> number -> number
> plus = \x -> \y -> x + y
<function> : number -> number -> number

curried 함수를 활용한 부분 애플리케이션:

> plus3 = plus 3
<function> : number -> number
> plus3 5
8 : number
> plus3 3.0
6 : Float

number 타입 캐스팅을 어떻게 할 것인가. number -> Int는 만들 수 없지만 어짜피 number는 Int가 필요할 때 자동으로 변하니 그냥 작성.

> toInt n = n // 1
<function> : Int -> Int
> plusInt x y = (toInt x) + y
<function> : Int -> Int -> Int
> plusInt x y = (toInt x  + y)
<function> : Int -> Int -> Int

타입 어노테이션 Type Annotations

대부분의 ML 방언과 같이 자동으로 처리하지만 최상위 레벨에서 수동으로 지정해야 좋은 경우도 있음. 예제 IntroML.elm 참조.

plus : number -> number -> number
plus x y = x + y

plusInt : Int -> Int -> Int
plusInt x y = x + y

plusInt : Int -> Int -> Int
plusInt = plus

앞에서 toInt 쓴 것과 달리 plusInt에서 명시적으로 어노테이션을 지정하고 위처럼 쓸 수 있음. 클라이언트에게 공개되는 API보다 더 범용적인 코드를 구현하는 것을 강조.

모듈 불러오기 Importing Modules

앞서 IntroML.elm을 받는다. exposing을 사용하게 변경되었다. (..)은 모듈 내 모든 함수를 노출하게 된다. 모듈을 약어로 부를 때는 as 키워드를 사용한다.

> import IntroML
> IntroML.plusInt 2 3
5 : Int
> import IntroML exposing (plusInt)
> plusInt 2 3
5 : Int
> import IntroML exposing (..)
> exclaim "Awesome"
"Awesome!" : String
> import IntroML as M
> m.plusInt 2 3
5 : Int

Basics, Maybe 등이 포함된 일반 라이브러리는 기본으로 불러오게 됨.

조건문

> if 1 == 1 then "Yes" else "No"
"Yes" : String
> if False then 1.0 else 1
1 : Float
> if | 1 == 1 -> 1.0 \
|    | 1 == 2 -> 1
1 : Float
> if | 1 == 1     -> 1.0 \
|    | otherwise  -> 1
1 : Float

otherwise는 True : Bool. 다중조건문 multi-way-if는 조건 중 참으로 평가되는 경우가 없으면 런타임 에러가 발생. 실행되지 않을 조건이 있다면? 닿지 않는 코드는 평가도 하지 않음. 다중조건문에서 줄 맞추는 것 잊지 말 것.

다형성 타입 Polymorphic Types

타입변수는 소문자로 시작하거나 한글자로 지정되는 경우가 많음.

> choose b x y = if b then x else y
<function> : Bool -> a -> a -> a
> choose True True False
True : Bool
> choose True "a" "b"
"a" : String
> choose True 1.0 2.0
1 : Float
> choose True 1 2
1 : number
> choose True 1 2.0
1 : Float

만약 다음과 같이 타입 어노테이션을 지정한다면 다형성 타입이지만 타입을 강제할 수 있음.

choose : Bool -> number -> number -> number

Basics에서 작성된 비교 연산자에는 comparable 이라는 특수 목적의 타입 변수가 존재함.

> (<)
<function> : comparable -> comparable -> Bool
> 1 < 2
True : Bool
> 1 < 2.0
True : Bool
> "a" < "ab"
True : Bool
> (2, 1) < (1, 2)
False : Bool
> (1 // 1) < 2.0 -- 타입 불일치
> True < False -- 타입 불일치

딴짓: 버그잡기

버그가 이미 잡혀서 내용 결과가 다름. 딴짓 실패.

버그를 찾으면 메일링 리스트에서 검색하고 버그 리포트를 남기자는 이야기.

리스트

cons 연산자는 코스에선 제안된 기능인데 이미 반영되서 import 할 필요 없음. 제안 기능은 불러와서 쓸 수 있는 모양. (::)는 OCaml 문법 , 는 하스켈 문법이라고.

> 1::2::3::4::[]
[1,2,3,4] : List number
> [1,2,3,4]
[1,2,3,4] : List number
> [1..6]
[1,2,3,4,5,6] : List number
> [1.0..6.0]
[1,2,3,4,5,6] : List Float

하스켈은 String이 List Char인데 여긴 아니라고.

> ['a','b','c']
['a','b','c'] : List Char
> "abc"
"abc" : String
> ['a','b','c'] == "abc" -- 타입 불일치

case 한줄로 쓰기. (갑자기 난이도 점프를 시도한 느낌.)

> len xs = case xs of {_::xs -> 1 + len xs; [] -> 0}
> len [1..10]
10 : number
> len ['a', 'b', 'c']
3 : number

글자 수를 세는 len이라는 함수를 정의했다. 리스트를 xs로 받아서 리스트 가장 앞에 녀석을 하나 빼 글자수 1을 더하고 나머지 xs를 다시 len 함수에 보내 계속 글자 수를 알아낸다. 계속 반복해서 빈 리스트 []가 되면 0을 반환해 전체 글자 수를 얻게 된다!

case가 모든 결과를 처리하지 못하면 완전하지 않은 패턴이 발견되었다고 런타임 에러가 발생한다. 다중 case문에서는 두번째 줄부터 맨 앞에 공백을 넣어야 한다.

> hd xs = case xs of \
|   x::_ -> x
<function> : List a -> a
> hd [2]
2 : number
> hd []
Error: Runtime error in module Repl (between lines 4 and 5)
Non-exhaustive pattern match in case-expression.
Make sure your patterns cover every case!

고차함수

> import List exposing (filter, map, foldr, foldl)
> filter
<function> : (a -> Bool) -> List a -> List a
> filter (\x -> x `rem` 2 == 0) [1..10]
[2,4,6,8,10] : List Int
> map
<function> : (a -> b) -> List a -> List b
> map(\x -> x ^ 2) [1..10]
[1,4,9,16,25,36,49,64,81,100] : List number
> foldr
<function: foldr> : (a -> b -> b) -> b -> List a -> b
> foldr (\x acc -> x :: acc) [] [1..10]
[1,2,3,4,5,6,7,8,9,10] : List number
> foldl
<function: foldl> : (a -> b -> b) -> b -> List a -> b
> foldl (\x acc -> x :: acc) [] [1..10]
[10,9,8,7,6,5,4,3,2,1] : List number

(::)도 함수라서 다음처럼 가능.

> (::)
<function> : a -> List a -> List a
> foldl (\x acc -> (::) x acc) [] [1..10] -- eta-expanded version
[10,9,8,7,6,5,4,3,2,1] : List number
> foldl (::) [] [1..10] -- eta-reduced version
[10,9,8,7,6,5,4,3,2,1] : List number

데이터타입

리스트는 귀납형 데이터 타입이라 직접 데이터 타입을 정의 할 수 있음. (enum 같은 느낌.) 값을 가지지 않을 수도, 가질 수도 있음.

> type Diet = Herb | Carn | Omni | Other String
> Carn
Carn : Repl.Diet
> Omni
Omni : Repl.Diet
> Other "Lactose"
Other "Lactose" : Repl.Diet
> Other -- Non-nullary data constructor는 그 자체로 함수
<function> : String -> Repl.Diet
> diets = [Herb, Omni, Omni, Other "Lactose"]
[Herb,Omni,Omni,Other "Lactose"] : List Repl.Diet

패턴매칭에 유용. 결과가 나오지 않는 case를 작성하지 않도록 주의.

> isHerb d = case d of \
|   Herb -> True \
|   _    -> False
<function> : Repl.Diet -> Bool
> List.map isHerb diets
[True,False,False,False] : List Bool

에러를 위한 타입

앞에서 작성한 hd 함수는 빈 리스트를 넣었을 때 런타임 에러가 발생하고 실패함. 에러를 위한 타입을 만들어서 의미있는 결과를 받도록 처리.

> type MaybeInt = YesInt Int | NoInt
> hd xs = case xs of \
|   x::xs' -> YesInt x \
|   []     -> NoInt
<function> : List Int -> Repl.MaybeInt
> hd [1..4]
YesInt 1 : Repl.MaybeInt
> hd []
NoInt : Repl.MaybeInt

다형성 타입으로 바꾸면,

> type MaybeData a = YesData a | NoData
> hd xs = case xs of \
|   x::_ -> YesData x\
|   []   -> NoData
<function> : List a -> Repl.MaybeData a
> hd [1]
YesData 1 : Repl.MaybeData number
> hd ['a','b','c']
YesData 'a' : Repl.MaybeData Char
> hd []
NoData : Repl.MaybeData a

이 방식은 MaybeData 패턴으로 엄청 일반적이고 Maybe라는 라이브러리도 포함되어 있음.

> type Maybe a = Just a | Nothing
> hd xs = case xs of \
|   x::_ -> Just x \
|   []   -> Nothing
<function> : List a -> Repl.Maybe a

Maybe 패턴을 활용한 Result 가 제공되고 있음. 다음은 예시에서의 코드.

errHead : List a -> Result String a
errHead xs = case xs of
  x::_ -> Ok x
  []   -> Err "errHead: expects non-empty list"

저장해서 불러오면 다음과 같은 결과.

> import MaybeStudy exposing (errHead)
> errHead ["What", "WHhooo"]
Ok "What" : Result.Result String String
> errHead []
Err ("errHead: expects non-empty list") : Result.Result String a

이항 연산자 infix Operators

(<|), (|>), (<<), (>>)를 Basics 문서에서 찾아보라고.

(<|)는 괄호를 입력해야 하는 번거로움을 줄여준다.

leftAligned (monospace (fromString "code"))
leftAligned << monospace <| fromString "code"

(|>)도 괄호를 줄여주고 역순으로 입력하기 때문에 이해하기 더 쉬운 코드가 된다.

scale 2 (move (10,10) (filled blue (ngon 5 30)))
ngon 5 30
  |> filled blue
  |> move (10,10)
  |> scale 2

위 둘은 괄호를 회피하는 것 이외에도 앞서 함수의 치역과 뒤따라오는 함수의 정의역을 일치시키기 위해서 결괏값을 먼저 처리하는 것 같다.

(<<)와 (>>)는 함수 합성에 사용한다.

(g << f) == (\x -> g (f x))
(g >> f) == (\x -> f (g x))

Let 표현식

지역 스코프 한정 변수를 let으로 지정해 선언한다. 앞 공백이 중요함. 그 밑은 같은 결과, 쉬운 표현.

plus3 a =
  let b = a + 1 in
  let c = b + 1 in
  let d = c + 1 in
    d

plus3 a =
  let b = a + 1
      c = b + 1
      d = c + 1
  in
    d

plus3 a = a |> plus 1 |> plus 1 |> plus 1

plus3 = plus 1 << plus 1 << plus 1

읽을 거리

필수

추천

그외

FP in Elm 노트 – 코스 개요

😢 이 페이지는 다음 주소로 변경될 예정입니다.

seoh님의 Elm Resources 글에서 Functional Programming: Purely Functional Data Structures in Elm
강의를 알게 되었다. 개요를 읽고 흥미가 생겨 강의 노트를 읽기 시작했고 나중에 쉽게 찾아보려고 짤막하게라도 정리하기로 했다. 강의에서 사용된 elm이 이미 구버전이라서 최신 버전과 다른 부분이 있어 그 부분은 별도로 적었다. 조금 지나면 내 노트도 금방 구버전이 될 것 같은 기분이지만.

강의 노트 정리 페이지 목록보기


Course Overview

이 코스는 다음 두가지 테마로 진행된다.

  • 효과적인 데이터 구조를 구현하고 분석, 특히 함수형 프로그래밍 언어의 맥락에서
  • 인터렉티브 프로그래밍을 위한 함수형 리액티브 프로그래밍(FRP) 배우기

여기서 사용한 언어는 ML의 방언인 Elm. ML은 1970년대 개발되어 많은 영향을 주고 있다.

  • 평가 전략 Evaluation Strategy: (이른 평가 문법 위에서 동작하는) 지연 평가
  • 타입 Types: 정적 타입, 자동 타입 인터페이스 등 쿨한 기능이 있어 하스켈과는 조금 다르다고.
  • 부작용 Side Effects: 함수형 프로그래밍에서는 가변변수 등 절차형에서 제공하는 기능을 사용할 수 있지만 작게, 지역적으로 사용하길 권장. ML은 타입 시스템 바깥에서 처리(?)하고 하스켈은 타입으로 기록하는 방식으로 차이가 있음.

Elm은 Standard ML, OCaml, F#에 비해 적은 기능만 추가돼 작은 방언이라 얘기한다.

이 코스도 처음으로 진행되고 Elm도 작은 언어에 작은 커뮤니티라 삽질을 각오하고 코스에 임할 것.