elm ver0.18
학습 기록
Functional Programming에대한 사전 지식이 부족하면 튜토리얼 을 먼저 읽기 권함
- Why You Should Give Elm a Try : 왜 Elm을 써야하는가에대한 간략한 설명과 3개의 유투브 영상이 링크되어 있음
- 케빈TV : '나는 프로그래머다' 엠씨로 활동중이신 개발자분, 중간중간 잡담이 있지만 같이 공부하는 기분이 들게함 :)
강추!
- Let's be mainstream! User focused design in Elm - Curry On: elm개발한 Evan Czaplicki의 소개영상
- 프로젝트 폴더 생성
- 터미널에서 프로젝트 폴더로 이동후
elm package install elm-lang/html
- 프로젝트 루트패스에
Hello.eml
파일 생성 - 파일에 "Hello"메세지를 위한 코드 작성
module Hello exposing (..) -- (..) == all
import Html exposing (..) -- Html 렌더링용
main =
text "Hello"
- 터미널에서
elm-reactor
실행
언어자체에 웹앱을 만들수 있는 프레임웍이 내장되어 있음 model, view, update가 작동하는 방식이 프레임웍에 의존해서 처리됨
오브젝트나 값만을 주고 받는게 아니라 함수(처리 알고리즘)을 주고-받음(λ : 람다
)으로서 구조를 간결하고 유연하게 만든다
-- ver. Anonymous functions
\ a b -> a / b
> (\ a b -> a / b) 3 2 -- 이름이 없는 함수 일때는 함수가 어디서 끝나는지 알려주기 위해 '()'로 감싼다
1.5 : Float
-- ver. Named functions
> divide : Float -> Float -> Float
> divide a b= a / b
<function> : Float -> Float -> Float
> divide 3 2
1.5 : Float
-- 'divide 3' 하나의 파라미터만 보내면 어떠한 결과가 나올까?
> divide 3
<function> : Float -> Float -- 함수를 반환함, 함수를 변수에 넣어보자
> divdeThreeBy = divide 3
<function> : Float -> Float
> divideThreeBy 2
1.5 : Float
> divideThreeBy 3
1 : Float
-- ver. Actual
> divide x y = x / y
<function> : Float -> Float -> Float
> divide x = \y -> x / y
<function> : Float -> Float -> Float
> divide = \x -> (\y -> x / y)
<function> : Float -> Float -> Float
- 실제와 좀 다르지만 이해하기 쉬운 설명 : 마지막 -> Float 부분이 리턴.
- 좀더 어려운 설명 : 'divide : Float -> Float -> Float'은 실제로는 두개의 함수로 이루어져 있으며 맨뒤의 함수부터 앞쪽의 함수에 함수 자체를 반환 시켜서 연산을 함.
type alias Animal =
{ species : String
, age : Int
}
m = { species = "dog", age = 4 } -- 새로운 record를 생성한다
m2 = { m | age = 2 } -- m record를 카피해서 age 값만 변경한다
m3 = Animal "cat" 3 -- 새로운 record를 간소화한 문법으로 생성한다
OOP개념의 enum
과 유사한 개념, 단순히 타입뿐아니라 각 타입함수가 값을 받을 수 있다.
type User = Anonymous | Named String
userPhoto : User -> String
userPhoto user =
case user of
Anonymous ->
"anon.png"
Named name ->
"users/" ++ name ++ ".png"
activeUsers : List User
activeUsers =
[ Anonymous, Named "catface420", Named "AzureDiamond", Anonymous ]
photos : List String
photos =
List.map userPhoto activeUsers
-- [ "anon.png", "users/catface420.png", "users/AzureDiamond.png", "anon.png" ]
데이터의 형태를 정하지 않고 처리하기위한 패턴
> type List a = Empty | Node a (List a)
> ns = Node 1 (Node 2 (Node 3 Empty))
-- Node 1 (Node 2 (Node 3 Empty)) : Repl.List number
> nil = Empty
-- Empty : Repl.List a
> isEmpty list = \
| case list of\
| Empty -> True\
| Node _ _ -> False
-- <function> : Repl.List a -> Bool
> isEmpty ns
-- False : Bool
> isEmpty nil
-- True : Bool
> length list = \
| case list of \
| Empty -> 0 \
| Node _ next -> 1 + length next
<function> : Repl.List a -> number
> length nil
-- 0 : number
> length ns
-- 3 : number
> reverse list = \
| let \
| reverseP xs acc = \
| case xs of \
| Empty -> acc \
| Node a next -> reverseP next (Node a acc) \
| in \
| reverseP list Empty
-- <function> : Repl.List a -> Repl.List a
> reverse nil
-- Empty : Repl.List a
> reverse ns
-- Node 3 (Node 2 (Node 1 Empty)) : Repl.List number
> member a list = \
| case list of \
| Empty -> False \
| Node x next -> if a == x then True else member a next
-- <function> : a -> Repl.List a -> Bool
> member 2 ns
-- True : Bool
> member 4 ns
-- False : Bool
_
(underscore) 해당 값을 사용하지않음(무시)- List
a
: a를 써도 되고 어떤 String값이든 쓸수 있음 단 컨벤션은 lowercase로 시작. - type 생성시 위 예제에서 List가 타입의 역할을 하고 오른쪽의 (Empty, Node a)부분이 데이터 역할을 하게됨.
- Maybe: 타언어(Java, Javascript, Ruby, Python)에 있는 null에 해당하는 상황을 처리하기위해 만들어짐. Swift의
optional
과 비슷한 개념 - Result: exeption을 처리하기 위해 만들어짐.
- Task: Result와 비슷하나
asynchronos
상황에 특화되어 만들어짐.
옵셔널이 필요한이유: 유저에게 정보입력 요구를 분할하라. ex)처음가입할때 이메일만 요구하고 사용하면서 이름, 성별등의 정보를 분할 요청하라 --> UX관점에서도 중요한 포인트
> type Maybe a = Nothing | Just a
> Nothing
Nothing : Repl.Maybe a
> Just
<function> : a -> Repl.Maybe a
> Just 3
Just 3 : Repl.Maybe number
> Just "frank"
Just "frank" : Repl.Maybe String -- 아마 스트링이 있을걸
Record 생성시 Maybe 사용
type alias User =
{ name : String
, age : Maybe Int
}
sue : User
sue =
{ name = "Sue", age = Nothing }
tom : User
tom =
{ name = "Tom", age = Just 24 }
canBuyAlcohol : User -> Bool
canBuyAlcohol user =
case user.age of -- 옵셔널로 설정된 값을 사용하려면 case문으로 풀어서 사용해야한다.
Nothing ->
False
Just age ->
age >= 21
- Swift비교: optional 사용은 Swift가 편하다고 느껴짐, 하지만
sue.age!
처럼 강제로 무시가능(nil
이 없는 것이 아님)
###Result 결과가 성공일수도 있고 실패일수도 있는 상황에서 사용됨, Error가 데이터라는것이 타언어와 다른특징.
> String.toInt
<function> : String -> Result.Result String Int
type Result error value
= Err error
| Ok value
> import String
> String.toInt "128"
Ok 128 : Result String Int
> String.toInt "64"
Ok 64 : Result String Int
> String.toInt "BBBB"
Err "could not convert string 'BBBB' to an Int" : Result String Int
view : String -> Html msg
view userInputAge =
case String.toInt userInputAge of
Err msg ->
span [class "error"] [text msg]
Ok age ->
if age < 0 then
span [class "error"] [text "I bet you are older than that!"]
else if age > 140 then
span [class "error"] [text "Seems unlikely..."]
else
text "OK!"
string : Decoder String -- string을 담고 있는 UnionType
int : Decoder Int
float : Decoder Float
bool : Decoder Bool
-- Json data를 Decoder 타입에 따라 변환해줌
decodeString : Decoder a -> String -> Result String a
> import Json.Decode exposing (..)
> decodeString int "42"
Ok 42 : Result String Int
> decodeString float "3.14159"
Ok 3.14159 : Result String Float
> decodeString bool "true"
Ok True : Result String Bool
> decodeString int "true" -- int 포멧으로 변환하려는데 Bool에 해당하는 스트링을 입력하니 컴파일 에러남.
Err "Expecting an Int but instead got: true" : Result String Int
> import Json.Decode exposing (..)
> int -- 인트 디코더
<decoder> : Json.Decode.Decoder Int
> list -- 리스트 디코더
<function> : Json.Decode.Decoder a -> Json.Decode.Decoder (List a)
> list int - 인트를 가지고있는 리스트 디코더
<decoder> : Json.Decode.Decoder (List Int)
> decodeString (list int) "[1, 2, 3]"
Ok [1,2,3] : Result.Result String (List Int)
-- triple quote 사용 가능
> decodeString (list string) """["hi", "yo"]"""
Ok ["hi","yo"] : Result.Result String (List String)
-- 리스트안에 리스트안까지 쉽게
> decodeString (list (list int)) "[ [0], [1,2,3], [4,5] ]"
Ok [[0],[1,2,3],[4,5]] : Result String (List (List Int))
커스텀 decoder를 생성할 때 사용한다.
field "x" int
x
: 필드명int
: int로 변환 가능한 데이터
> field
<function> : String -> Json.Decode.Decoder a -> Json.Decode.Decoder a
Decode_json.elm
module Decode_json exposing (..)
import Json.Decode exposing (..)
nameExtractor : Decoder String
nameExtractor = field "name" string
me = """
{ "id": 1
, "name": "Frank"
}
"""
> import Decode_json exposing (..)
> import Json.Decode exposing (..)
> decodeString
<function> : Json.Decode.Decoder a -> String -> Result.Result String a
> decodeString nameExtractor me
Ok "Frank" : Result.Result String String
map2 함수를 이용해서 두개의 디코더를 합성보자
> import Decode_json exposing (..)
> type alias Person = { id : Int, name : String }
<function> : Int -> String -> Repl.Person
> personDecoder = map2 Person (field "id" int) (field "name" string)
<decoder> : Json.Decode.Decoder Repl.Person
> map2
<function>
: (a -> b -> value)
-> Json.Decode.Decoder a
-> Json.Decode.Decoder b
-> Json.Decode.Decoder value
> decodeString personDecoder me
Ok { id = 1, name = "Frank" } : Result.Result String Decode_json.Person
2개가 아닌 더 많은 갯수의 디코더를 NoRedInk/elm-decode-pipeline로 합성해 보자
먼저 NoRedInk/elm-decode-pipeline를 설치해줘야 한다.
elm-package.json >dependencies
에"NoRedInk/elm-decode-pipeline": "3.0.0 <= v < 4.0.0"
추가elm-stuff
폴더 삭제terminal에서elm-make
위는
elm-package installer
를 사용하지않고 수동으로 한것 아래처럼 터미널에서 자동으로 설치하자
$ elm-package install NoRedInk/elm-decode-pipeline
import Json.Decode exposing (..)
import Json.Decode.Pipeline exposing (decode, required)
type alias Point = { x : Int, y : Int }
pointDecoder : Decoder Point
pointDecoder =
decode Point
|> required "x" int
|> required "y" int
pointJsonString = """
{ "x": 23
, "y": 78
}
"""
> decodeString pointDecoder pointJsonString
Ok { x = 23, y = 78 } : Result.Result String Decode_json.Point
더 쉽게 해보자! json_to_elm에서 제공하는 웹페이지에서 Json 스트링을 넣어주면 자동으로 incode/decode를 할 수 있는 elm code를 생성해 준다
- http://noredink.github.io/json-to-elm/ 에서 코드를 생성후 프로젝트에 붙여 넣는다
- 생성된 코드에서 json-extra를 사용한다고 경고가 나온다
elm-package.json > elm-community/json-extra": "2.1.0 <= v < 3.0.0" 추가
$ elm-package install elm-community/json-extra
module JsonToElm exposing (..)
import Json.Encode
import Json.Decode
-- elm-package install -- yes noredink/elm-decode-pipeline
import Json.Decode.Pipeline
type alias User =
{ id : Int
, email : String
, name : String
}
decodeUser : Json.Decode.Decoder User
decodeUser =
Json.Decode.Pipeline.decode User
|> Json.Decode.Pipeline.required "id" (Json.Decode.int)
|> Json.Decode.Pipeline.required "email" (Json.Decode.string)
|> Json.Decode.Pipeline.required "name" (Json.Decode.string)
encodeUser : User -> Json.Encode.Value
encodeUser record =
Json.Encode.object
[ ("id", Json.Encode.int <| record.id)
, ("email", Json.Encode.string <| record.email)
, ("name", Json.Encode.string <| record.name)
]
--------------
frank = """
{ "id": 1
, "email": "[email protected]"
, "name": "Frank"
}
"""
- 위코드는 NoRedInk/elm-decode-pipeline를 사용하는 코드임
> import JsonToElm exposing (..)
> frank
"\n { \"id\": 1\n , \"email\": \"[email protected]\"\n , \"name\": \"Frank\"\n }\n"
: String
> decodeString
<function> : Json.Decode.Decoder a -> String -> Result.Result String a
> decodeString decodeUser frank
Ok { id = 1, email = "[email protected]", name = "Frank" }
: Result.Result String JsonToElm.User
Spelling.elm -> Spelling.js 로 컴파일 후 Html에 임베딩한후 다른 자바스크립트 코드와 통신하도록 하는 구조
아래 예제는 유저가 텍스트 필드에 입력했을때 스펠링을
체크(Javascript code)
해서 후보 단어들을 텍스트 필드 아래에 보여주는 앱.
- index.html
<div id="spelling"></div>
<script src="spelling.js"></script>
<script>
var app = Elm.Spelling.fullscreen();
app.ports.check.subscribe(function(word) {
var suggestions = spellCheck(word);
app.ports.suggestions.send(suggestions);
});
function spellCheck(word) {
// dummy implementation
if (word == "helo") {
return ["hello"]
}else if (word == "hell") {
return ["hello", "hell"]
}
return [];
}
</script>
- Spelling.elm
port module Spelling exposing (..)
import Html exposing (..)
import Html.Events exposing (..)
import String
main : Program Never Model Msg
main =
Html.program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
-- MODEL
type alias Model =
{ word : String
, suggestions : List String
}
init : (Model, Cmd Msg)
init =
(Model "" [], Cmd.none)
-- UPDATE
type Msg
= Change String
| Check
| Suggest (List String)
port check : String -> Cmd msg
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Change newWord ->
( Model newWord [], Cmd.none )
Check ->
( model, check model.word )
Suggest newSuggestions ->
( Model model.word newSuggestions, Cmd.none )
-- SUBSCRIPTIONS
port suggestions : (List String -> msg) -> Sub msg
subscriptions : Model -> Sub Msg
subscriptions model =
suggestions Suggest
-- VIEW
view : Model -> Html Msg
view model =
div []
[ input [ onInput Change ] []
, button [ onClick Check ] [ text "Check" ]
, div [] [ text (String.join ", " model.suggestions) ]
]
- compile Spelling.elm file
elm-make Spelling.elm --output=spelling.js
elm앱이 시작(init)할때 특정값(ex. user)을 받고 싶을때 사용함. elm앱 생성시 program 대신 programWithFlags 사용
type alias Flags =
{ user : String
, token : String
}
init : Flags -> ( Model, Cmd Msg )
init flags =
...
main =
programWithFlags { init = init, ... }
var app = Elm.MyApp.fullscreen({
user: 'Tom',
token: '12345'
});
var node = document.getElementById('my-app');
var app = Elm.MyApp.embed(node, {
user: 'Tom',
token: '12345'
});