효과 타입과 핸들러를 갖춘 강타입 함수형 스타일 언어 Koka의 설치, 실행 방법과 핵심 개념을 소개하고 언어 사양 초안을 포함합니다.
Koka에 오신 것을 환영합니다. Koka는 효과 타입과 핸들러를 갖춘 강타입 함수형 스타일 언어입니다.
왜 Koka인가?Koka 둘러보기설치토론 포럼Github라이브러리
참고: Koka v3는 현재 개발 중인 연구용 언어이며, 프로덕션 사용을 위해 준비되지 않았습니다. 그럼에도 언어는 안정적이며 컴파일러는 전체 사양을 구현합니다. 현재 가장 부족한 부분은 (async) 라이브러리와 패키지 관리입니다.
뉴스:
2025-07-22: Koka v3.2.2 릴리스. 버그 수정 릴리스.
2025-07-17: Koka v3.2.0 릴리스. 이제 생성자 컨텍스트에서 구멍을 나타내기 위해 키워드 hole을 사용하며, 밑줄은 인자를 자동으로 eta-확장하는 데 사용됩니다(예: [1].map(inc(_)))). 암시적 해결은 이제 유일한 해를 요구하지만, 스코핑을 고려합니다. 더 이상 qualified 타입을 사용하지 않고, 대신 컴파일러가 해결하는 암시적 phantom 매개변수를 사용합니다. 자세한 내용은 whatsnew를 참고하세요.
2025-07-17: PLDI'25의 distinguished paper award 두 편이 Koka를 사용하는 논문에 수여되었습니다. “Practical Type Inference with Levels”, Andong Fan, Han Xu, Ningning Xie (pdf)와 “Principal Type Inference under a Prefix”, Daan Leijen, Wenjia Ye (pdf)입니다. 후자는 Koka 언어에서 사용되는 정적 오버로딩 해결을 정형화합니다.
2025-07-03: Koka v3.1.3 릴리스. 적용자(applier) 문법 .()를 추가하여 x.f.(42)가 (x.f)(42)의 설탕 문법이 되며, 구조체에서 선택한 함수를 호출할 때 편리합니다. 또한 지연 생성자(lazy constructors)를 지원합니다 — Anton Lorenzen, Daan Leijen, Wouter Swierstra, Sam Lindley의 “First-order Laziness” (ICFP'25) pdf를 참고하세요.
2024-05-30: VS Code 워크스페이스 밖에서의 Koka 설치를 수정한 Koka v3.1.2 릴리스.
2024-05-30: Anton Lorenzen, Daan Leijen, Sam Lindley, Wouter Swierstra의 “The Functional Essence of Binary Search Trees” 논문을 읽어보세요. 6월 27일 PLDI'24에서 발표될 예정입니다. paper
2024-05-30: Collège de France에서 제어 구조와 대수적 효과에 관한 Xavier Leroy의 훌륭한 강연 시리즈 일부로, Koka에서 효율적인 효과 핸들러의 설계와 컴파일에 대한 강연을 볼 수 있습니다(다른 초청 강연들도 다수 온라인 제공).
2024-03-04: 언어 서버 크래시 수정이 포함된 Koka v3.1.1 릴리스.
2024-02-14: 동시 빌드 시스템과 stdio 프로토콜에서 개선된 언어 서비스를 포함한 Koka v3.1.0 릴리스. 정형 시스템과 더 가깝도록 명명된(named) 효과 타이핑을 재설계했습니다. 예시는 samples/handlers/named를 참고하세요.
2024-01-25: VS Code hover 및 인레이(inlay) 정보가 개선된 Koka v3.0.4 릴리스. std/core를 여러 모듈로 분리하고, 암시적(infinite expansion) 무한 확장 버그 및 기타 여러 소소한 개선을 수정했습니다.
2024-01-13: VS Code 통합과 인레이 힌트가 개선된 Koka v3.0.1 릴리스. 로컬 qualified 이름과 암시적 매개변수의 초기 지원( samples/syntax 참고). 다양한 소소한 버그 수정.
2023-12-30: VS Code 언어 통합(타입 정보, 정의로 이동, 에디터에서 테스트 함수 직접 실행, 자동 Koka 설치 등)과 여러 기능을 포함한 Koka v2.6.0 릴리스. 이를 가능하게 만든 Tim Whiting과 Fredrik Wieczerkowski에게 특별한 감사를 전합니다!
2023-12-27: 기술 보고서 “The functional essence of binary trees” 업데이트. 완전한 in-place 프로그래밍과 새로운 hole 컨텍스트를 사용하여 이진 탐색 트리 알고리즘의 완전 검증된 함수형 구현을 만들며, 성능은 명령형 C 구현과 대등합니다.
2023-07-03: Koka v2.4.2 릴리스: ICFP'23의 “FP 2: Fully in-Place Functional Programming”에서 설명된 fip 및 fbip 키워드 지원 추가 [pdf]. 다양한 수정 및 성능 개선.
2021-02-04 (고정) Context Free 유튜브 채널에서 Koka의 효과(그리고 다른 12 (!)개 언어)에 대한 짧고 재미있는 영상을 게시했습니다.
2021-09-01 (고정) ICFP'21 튜토리얼 “Programming with Effect Handlers and FBIP in Koka”가 youtube에 공개되었습니다.
2022-02-07: Koka v2.4.0 릴리스: 특수화와 int 연산 개선, rbtree-fbip 샘플 추가, 문법 개선(pub(기존 public 대신), private 제거(기본이 private), final ctl(기존 brk 대신), 숫자 리터럴의 밑줄, double을 float64로 변경 등), 다양한 버그 수정.
2021-12-27: Koka v2.3.8 릴리스: int 성능 개선, 다양한 버그 수정, wasm 백엔드 업데이트, 초기 conan 지원, js 백엔드 수정.
2021-11-26: Koka v2.3.6 릴리스: maybe 유사 타입은 이미 값 타입이지만, 이제 중첩되지 않으면 힙 할당이 더 이상 필요 없음(그리고 [Just(1)]은 [1]과 동일한 힙 공간 사용), 원자적 refcounting 개선(Anton Lorenzen), 특수화 개선(Steven Fontanella), 여러 소소한 수정, std/os/readline 추가, freeBSD 빌드 수정.
2021-10-15: Koka v2.3.2 릴리스. 초기 wasm 지원(--target=wasm 사용, emscripten과 wasmtime 설치), 재사용 특수화 개선(Anton Lorenzen), 다양한 버그 수정.
2021-09-29: Koka v2.3.1 릴리스. TRMC 최적화 개선, 재사용 개선( rbtree 벤치마크가 이제 C++만큼 빠름), 효과 연산 더 빠름. 실험적: 익명 함수 표현식에서 -> 생략 허용(예: xs.map( fn(x) x + 1 )) 및 연산 절(clauses). 커맨드 라인 옵션이 다소 변경되었고 .koka가 표준 출력 디렉터리.
2021-09-20: Koka v2.3.0 릴리스. 새로운 brace elision 및 괄호 없는 if/match 조건. ES6 모듈과 BigInt를 사용하도록 javascript 백엔드를 업데이트. 새 module std/num/int64, 효과 연산 성능 개선.
2021-09-05: Koka v2.2.1 릴리스. 초기 병렬 태스크, binary-trees 벤치마크, 그리고 brace elision.
2021-08-26: Koka v2.2.0 릴리스. 단순화 개선(Rashika B), 모듈 간 특수화(Steven Fontanella), 빌림(borrowing) 주석과 재사용 분석 개선(Anton Lorenzen).
2021-08-26: 12:30 EST에 ICFP'21에서 Koka 튜토리얼이 생중계되었으며, youtube에서 볼 수 있습니다.
2021-08-23: Ningning Xie와 Daan Leijen의 “Generalized Evidence Passing for Effect Handlers”가 ICFP'21에서 발표되었습니다. youtube에서 보거나 paper를 읽어보세요.
2021-08-22: Youyou Cong, Ningning Xie, Daan Leijen의 “First-class Named Effect Handlers”가 HOPE'21에서 발표되었습니다. youtube에서 보거나 paper를 읽어보세요.
2021-06-23: Koka v2.1.9 릴리스, 초기 모듈 간 특수화(Steven Fontanella).
2021-06-17: Koka v2.1.8 릴리스, 초기 Apple M1 지원.
Perceus 논문이 PLDI'21에서 distinguished paper award를 수상했습니다!
2021-06-10: Koka v2.1.6 릴리스.
2021-05-31: Koka v2.1.4 릴리스.
2021-05-01: Koka v2.1.2 릴리스.
2021-03-08: Koka v2.1.1 릴리스.
2021-02-14: Koka v2.0.16 릴리스.
2020-12-12: Koka v2.0.14 릴리스.
2020-12-02: Koka v2.0.12 릴리스.
2020-11-29: Perceus 기술 보고서 게시 (pdf).
Koka를 시작하는 가장 쉬운 방법은 훌륭한 VS Code 편집기를 사용하고 Koka 확장을 설치하는 것입니다. 확장 패널로 가서 Koka를 검색하고 오른쪽에 보이는 것처럼 공식 확장을 설치하세요.
확장을 설치하면 플랫폼용 최신 Koka 컴파일러 설치도 안내합니다(Windows x64, MacOS x64 및 arm64, Linux x64에서 제공).
설치가 끝나면 samples 디렉터리가 열립니다. 또한 커맨드 패널(Ctrl/Cmd+Shift+P)에서 Koka: Open samples 명령을 실행해 수동으로 열 수도 있습니다(입력을 시작하면 명령이 위로 올라옵니다). 예를 들어 basic/caesar.kk 파일을 열어보세요. run debug(또는 optimized)를 클릭하면 Koka가 함수를 컴파일하고 실행하며, 출력은 VS Code 터미널에 표시됩니다.
ctrl+alt(MacOS에서는 ctrl+option)를 누른 채로 있으면 인레이 힌트가 표시되며, 추론된 타입, 완전 수식된 이름, 암시적 인자 등을 보여줍니다.
대비
Windows(x64)에서는 cmd 프롬프트를 열고 다음을 실행하세요:
curl -sSL -o %tmp%\install-koka.bat https://github.com/koka-lang/koka/releases/latest/download/install.bat && %tmp%\install-koka.bat
Linux(x64) 및 macOS(x64, arm64(M1/M2))에서는 다음으로 Koka를 설치할 수 있습니다:
curl -sSL https://github.com/koka-lang/koka/releases/latest/download/install.sh | sh
(이전에 macOS에서 brew로 Koka를 설치했다면 먼저 brew uninstall koka를 수행하세요). 다른 플랫폼에서는 보통 소스에서 Koka를 빌드하는 것이 쉽습니다.
설치 후 Koka가 올바르게 설치되었는지 확인하세요:
$ koka
_ _
| | | |
| | _ ___ | | _ __ _
| |/ / _ \| |/ / _' | welcome to the koka interactive compiler
| ( (_) | ( (_| | version 2.4.0, Feb 7 2022, libc x64 (gcc)
|_|\_\___/|_|\_\__,_| type :? for help, and :q to quit
loading: std/core
loading: std/core/types
loading: std/core/hnd
>
대화형 환경을 종료하려면 :q를 입력하세요.
자세한 설치 지침 및 다른 플랫폼은 releases 페이지를 참고하세요. 또한 컴파일러를 소스에서 빌드하는 것도 간단합니다.
VS Code 편집기를 사용할 때는, 이름이 main, example..., test...인 public 함수를 편집기 환경에서 바로 컴파일하고 실행할 수 있다는 점에 유의하세요.
물론 커맨드 라인에서 컴파일러를 직접 실행하거나 대화형 환경을 사용할 수도 있습니다.
Koka 소스는 다음처럼 컴파일할 수 있습니다(모든 samples는 사전 설치됨):
$ koka samples/basic/caesar.kk
compile: samples/basic/caesar.kk
loading: std/core
loading: std/core/types
...
check : samples/basic/caesar
linking: samples_basic_caesar
created: .koka/v2.3.1/gcc-debug/samples_basic_caesar
그리고 생성된 실행 파일을 실행합니다:
$ .koka/v2.3.1/gcc-debug/samples_basic_caesar
plain : Koka is a well-typed language
encoded: Krnd lv d zhoo-wbshg odqjxdjh
cracked: Koka is a well-typed language
-O2 플래그는 최적화된 프로그램을 빌드합니다. 이를 순수 함수형으로 구현된 레드-블랙 트리의 균형 삽입(rbtree.kk)에 적용해봅시다:
$ koka -O2 -o kk-rbtree samples/basic/rbtree.kk
...
linking: samples_basic_rbtree
created: .koka/v2.3.1/gcc-drelease/samples_basic_rbtree
created: kk-rbtree
$ time ./kk-rbtree
420000
real 0m0.626s
(Windows에서는 경과 시간을 보기 위해 --kktime 옵션을 줄 수 있습니다). 이를 stl::map을 사용하는 in-place 갱신 C++ 구현(rbtree.cpp)과 비교할 수 있습니다(내부적으로도 red-black tree를 사용):
$ clang++ --std=c++17 -o cpp-rbtree -O3 /usr/local/share/koka/v2.3.1/lib/samples/basic/rbtree.cpp
$ time ./cpp-rbtree
420000
real 0m0.667s
여기서 C++에 견줄 만한 뛰어난 성능(AMD 5950X의 Ubuntu 20.04)은, Perceus가 순수 함수형 재균형(rebalancing)의 빠른 경로를 자동으로 변환하여 대부분 in-place 업데이트를 사용하게 만들고, 수작업으로 최적화된 C++ 라이브러리의 명령형 재균형 코드와 매우 유사하게 만들기 때문입니다.
입력 파일을 주지 않으면 기본으로 대화형 환경이 실행됩니다:
$ koka
_ _
| | | |
| | _ ___ | | _ __ _
| |/ / _ \| |/ / _' | welcome to the koka interactive compiler
| ( (_) | ( (_| | version 2.3.1, Sep 21 2021, libc x64 (clang-cl)
|_|\_\___/|_|\_\__,_| type :? for help, and :q to quit
loading: std/core
loading: std/core/types
loading: std/core/hnd
>
이제 몇 가지 식을 시험해볼 수 있습니다:
> println("hi koka")
check : interactive
check : interactive
linking: interactive
created: .koka\v2.3.1\clang-cl-debug\interactive
hi koka
> :t "hi"
string
> :t println("hi")
console ()
또는 데모를 로드할 수 있습니다(많이 입력하지 않도록 tab 완성을 사용하세요):
> :l samples/basic/fibonacci
> main()
...
The 10000th fibonacci number is 33644764876431783266621612005107543310302148460680063906564769974680081442166662368155595513633734025582065332680836159373734790483865268263040892463056431887354544369559827491606602099884183933864652731300088830269235673613135117579297437854413752130520504347701602264758318906527890855154366159582987279682987510631200575428783453215515103870818298969791613127856265033195487140214287532698187962046936097879900350962302291026368131493195275630227837628441540360584402572114334961180023091208287046088923962328835461505776583271252546093591128203925285393434620904245248929403901706233888991085841065183173360437470737908552631764325733993712871937587746897479926305837065742830161637408969178426378624212835258112820516370298089332099905707920064367426202389783111470054074998459250360633560933883831923386783056136435351892133279732908133732642652633989763922723407882928177953580570993691049175470808931841056146322338217465637321248226383092103297701648054726243842374862411453093812206564914032751086643394517512161526545361333111314042436854805106765843493523836959653428071768775328348234345557366719731392746273629108210679280784718035329131176778924659089938635459327894523777674406192240337638674004021330343297496902028328145933418826817683893072003634795623117103101291953169794607632737589253530772552375943788434504067715555779056450443016640119462580972216729758615026968443146952034614932291105970676243268515992834709891284706740862008587135016260312071903172086094081298321581077282076353186624611278245537208532365305775956430072517744315051539600905168603220349163222640885248852433158051534849622434848299380905070483482449327453732624567755879089187190803662058009594743150052402532709746995318770724376825907419939632265984147498193609285223945039707165443156421328157688908058783183404917434556270520223564846495196112460268313970975069382648706613264507665074611512677522748621598642530711298441182622661057163515069260029861704945425047491378115154139941550671256271197133252763631939606902895650288268608362241082050562430701794976171121233066073310059947366875
대화형 환경에서는 :set <options>로 커맨드 라인 옵션을 설정할 수 있습니다. 예를 들어 rbtree 예제를 다시 로드하고 --showtime으로 경과 시간을 출력해볼 수 있습니다:
> :set --showtime
> :l samples/basic/rbtree.kk
> main()
...
420000
info: elapsed: 4.104s, user: 4.046s, sys: 0.062s, rss: 231mb
그리고 -O2로 최적화를 켜고 다시 실행합니다(Windows에서 AMD 5950X):
> :set -O2
> :r
> main()
...
420000
info: elapsed: 0.670s, user: 0.656s, sys: 0.015s, rss: 198mb
마지막으로 인터프리터를 종료합니다:
> :q
I think of my body as a side effect of my mind.
-- Carrie Fisher (1956)
다음은?
새로운 언어는 많이 설계되고 있지만, Haskell의 순수 대 모나딕 프로그래밍이나 Rust의 빌림 검사처럼 근본적으로 새로운 개념을 가져오는 경우는 많지 않습니다. Koka는 효과 타이핑, 효과 핸들러, 그리고 Perceus 메모리 관리로 차별화됩니다.
Koka는 작고 직교적이며 잘 연구된 언어 기능의 핵심 집합을 갖습니다. 하지만 각각의 기능은 가능한 한 일반적이고 _조합 가능_하도록 설계되어, 추가적인 “특수” 확장을 필요로 하지 않습니다. 핵심 기능에는 일급 함수, 고차 랭크의 impredicative 다형 타입/효과 시스템, 대수적 데이터 타입, 그리고 효과 핸들러가 포함됩니다.
fun hello-ten() var i := 0 while { i < 10 } println("hello") i := i + 1 fun hello-ten() var i := 0 while { i < 10 } println("hello") i := i + 1
min-gen 설계 원리의 예로, Koka는 대부분의 제어 흐름 기본 요소를 일반 함수로 구현합니다. 익명 함수는 fn(){ <body> }로 쓸 수 있지만, 문법적 편의를 위해 인자가 없는 함수는 { <body> }처럼 중괄호만으로 더 줄일 수 있습니다. 또한 brace elision을 사용하면, 들여쓰기된 블록은 자동으로 중괄호가 붙습니다.
이제 일반 함수 호출로 whilestd/core/while: forall<e> (predicate : () -> <div|e> bool, action : () -> <div|e> ()) -> <div|e> () 루프를 예제처럼 쓸 수 있으며, whilestd/core/while: forall<e> (predicate : () -> <div|e> bool, action : () -> <div|e> ()) -> <div|e> () 호출은 whilestd/core/while: forall<e> (predicate : () -> <div|e> bool, action : () -> <div|e> ()) -> <div|e> ()( fn(){ i < 10 }, fn(){ ... } )로 디슈가됩니다.
이는 자연스럽게 _일관성_으로 이어집니다. 괄호 안의 식은 함수 호출 전에 항상 평가되는 반면, 중괄호 안의 식(아, 멜빵!)은 지연되어 평가되지 않을 수도 있고, 예제처럼 여러 번 평가될 수도 있습니다. 대부분의 다른 언어에서는 whilestd/core/while: forall<e> (predicate : () -> <div|e> bool, action : () -> <div|e> ()) -> <div|e> () 루프의 조건을 괄호로 쓰면서도 여러 번 평가될 수 있어 이런 점이 일관되지 않습니다.
Koka는 모든 함수의 효과를 타입에서 추론하고 추적하며, 함수 타입은 3가지 부분으로 구성됩니다: 인자 타입, 효과 타입, 결과 타입. 예를 들어:
fun sqr : (int) -> total int // total: 수학적으로 전체(총) 함수 fun divide : (int,int) -> exn int // exn: 예외를 던질 수 있음(부분 함수) fun turing : (tape) -> div int // div: 종료하지 않을 수 있음(발산) fun print : (string) -> console () // console: 콘솔에 출력할 수 있음 fun rand : () -> ndet int // ndet: 비결정적 fun sqr : (int std/core/types/int: V) -> total std/core/types/total: E int std/core/types/int: V fun divide : (int std/core/types/int: V,int std/core/types/int: V) -> exn std/core/exn/exn: (E, V) -> V int std/core/types/int: V fun turing : (tape) -> div std/core/types/div: X int std/core/types/int: V fun print : (string std/core/types/string: V) -> console std/core/console/console: X () fun rand : () -> ndet std/core/types/ndet: X int std/core/types/int: V
정밀한 효과 타이핑은 Koka에 견고한 의미론과 깊은 안전 보장을 제공하며, 잘 연구된 범주론에 의해 뒷받침됩니다. 이는 Koka를 사람과 컴파일러 모두에게 추론하기 쉽게 만듭니다. (효과 타이핑의 중요성 때문에, Koka라는 이름은 일본어로 _effective_를 의미하는 단어(効果, こうか, Kōka)에서 유래했습니다.)
어떤 효과도 없는 함수는 totalstd/core/types/total: E이라 부르며 수학적으로 전체 함수에 해당합니다 — 좋은 위치죠. 그 다음으로 예외를 던질 수 있는 부분 함수(exnstd/core/exn/exn: (E, V) -> V), 그리고 종료하지 않을 수 있는 함수 divstd/core/types/div: X(발산)가 있습니다. exnstd/core/exn/exn: (E, V) -> V와 divstd/core/types/div: X의 조합은 purestd/core/pure: E라 부르며 Haskell의 순수성 개념에 해당합니다. 그 위로는 가변성(ststd/core/types/st: H -> E<h>)부터 완전한 비결정적 부작용 iostd/core/io: E까지 있습니다.
효과도 다형적일 수 있습니다. 리스트에 함수를 매핑하는 것을 생각해봅시다:
fun map( xs : list<a>, f : a -> e b ) : e list<b> match xs Cons(x,xx) -> Cons( f(x), map(xx,f) ) Nil -> Nil fun map( xs : list std/core/types/list: V -> V<a>, f : a -> e b ) : e list std/core/types/list: V -> V<b> match xs Cons std/core/types/Cons: forall<a> (head : a, tail : list<a>) -> list<a>(x,xx) -> Cons std/core/types/Cons: forall<a> (head : a, tail : list<a>) -> list<a>( f(x), map(xx,f) ) Nil std/core/types/Nil: forall<a> list<a> -> Nil std/core/types/Nil: forall<a> list<a>
한 글자 타입은 다형 타입(즉 generic)이며, Koka는 요소 타입이 a인 리스트에서 요소 타입이 b인 리스트로 매핑한다고 추론합니다. map 자체는 고유한 효과가 없으므로, map을 적용한 효과는 적용되는 함수 f의 효과인 e와 정확히 같습니다.
min-gen 설계 원리의 또 다른 예: 예외, 제너레이터, async/await 등을 지원하기 위해 다양한 특수 언어/컴파일러 확장을 두는 대신, Koka는 대수적 효과 핸들러를 완전히 지원합니다. 이를 통해 async/await 같은 고급 제어 추상을 타입 안전하고 조합 가능한 방식으로 사용자 라이브러리로 정의할 수 있습니다.
다음은 intstd/core/types/int: V 값을 산출(yield)하기 위한 하나의 control (ctl) 연산을 가진 효과 정의 예시입니다:
effect yield ctl yield( i : int ) : bool effect why/yield: (E, V) -> V yield why/yield: (E, V) -> V ctl yield( i i: int : int std/core/types/int: V ) : bool std/core/types/bool: V
효과가 선언되면, 예를 들어 리스트의 요소들을 산출하는 데 사용할 수 있습니다:
fun traverse( xs : list<int> ) : yield () match xs Cons(x,xx) -> if yield(x) then traverse(xx) else () Nil -> () fun traverse why/traverse: (xs : list<int>) -> yield ()( xs xs: list<int> : list std/core/types/list: V -> V<int std/core/types/int: V> )result: -> yield () : yield why/yield: (E, V) -> V (std/core/types/unit: V)std/core/types/unit: V match xs xs: list<int> Cons std/core/types/Cons: forall<a> (head : a, tail : list<a>) -> list<a>(x x: int,xx xx: list<int>) -> if yield why/yield: (i : int) -> yield bool(x x: int) then traverse why/traverse: (xs : list<int>) -> yield ()(xx xx: list<int>) else (std/core/types/Unit: ())std/core/types/Unit: () Nil std/core/types/Nil: forall<a> list<a> -> (std/core/types/Unit: ())std/core/types/Unit: ()
traversewhy/traverse: (xs : list<int>) -> yield () 함수는 yieldwhy/yield: (i : int) -> yield bool를 호출하므로, 타입에 yieldwhy/yield: (E, V) -> V 효과가 들어갑니다. 따라서 traversewhy/traverse: (xs : list<int>) -> yield ()를 사용하려면 yieldwhy/yield: (E, V) -> V 효과를 _처리(handle)_해야 합니다. 이는 예외 핸들러를 정의하는 것과 유사하지만, (여기서는 intstd/core/types/int: V) 값을 받을 수 있고, 호출 지점으로 결과를 가지고 _재개(resume)_할 수 있습니다(여기서는 계속 순회할지 결정하는 boolean).
fun print-elems() : console () with ctl yield(i) println("yielded " ++ i.show) resume(i<=2) traverse([1,2,3,4]) fun print-elems why/print-elems: () -> console ()()result: -> console () : console std/core/console/console: X (std/core/types/unit: V)std/core/types/unit: V with handler: (() -> <yield,console> ()) -> console () ctl yield yield: (i : int, resume : (bool) -> console ()) -> console ()(i i: int) println std/core/console/string/println: (s : string) -> console ()("yielded "literal: string count= 8 ++std/core/types/(++): (x : string, y : string) -> console string i i: int.show std/core/int/show: (i : int) -> console string) resume resume: (bool) -> console ()(i i: int<=std/core/int/(<=): (x : int, y : int) -> console bool2 literal: int dec = 2 hex8 = 0x02 bit8 = 0b00000010) traverse why/traverse: (xs : list<int>) -> <yield,console> ()([std/core/types/Cons: forall<a> (head : a, tail : list<a>) -> list<a>1 literal: int dec = 1 hex8 = 0x01 bit8 = 0b00000001,2 literal: int dec = 2 hex8 = 0x02 bit8 = 0b00000010,3 literal: int dec = 3 hex8 = 0x03 bit8 = 0b00000011,4 literal: int dec = 4 hex8 = 0x04 bit8 = 0b00000100[]std/core/types/Nil: forall<a> list<a>](https://koka-lang.github.io/koka/doc/std_core_types.html#con_space_Nil))
with 문은 나머지 스코프(여기서는 traversewhy/traverse: (xs : list<int>) -> yield ()([1,2,3,4]))에 대해 yieldwhy/yield: (i : int) -> yield bool control 연산의 핸들러를 동적으로 바인딩합니다. yieldwhy/yield: (i : int) -> yield bool가 호출될 때마다 우리의 control 핸들러가 호출되어 현재 값을 출력하고, 호출 지점으로 boolean 결과를 가지고 재개합니다. 이런 동적 바인딩은 정적 타이핑을 유지하기 때문에 매우 안전합니다. 실제로 이 핸들러는 yieldwhy/yield: (E, V) -> V 효과를 방출(discharge)하며, 대신 consolestd/core/console/console: X 효과를 넣습니다(println 때문). 예제를 실행하면 다음을 얻습니다:
yielded: 1
yielded: 2
yielded: 3
Perceus는 Koka가 자동 메모리 관리를 위해 사용하는 컴파일러 최적화 참조 카운팅 기법입니다[13, 22]. 이것은 (증거 전달[23–25])과 결합되어, 가비지 컬렉터나 런타임 시스템 없이도 Koka가 순수 C 코드로 직접 컴파일될 수 있게 합니다.
Perceus는 강력한 정적 분석을 사용해 참조 카운트를 공격적으로 최적화합니다. 여기서 Koka의 강한 의미론적 기반이 큰 도움이 됩니다: 귀납적 데이터 타입은 사이클을 만들 수 없고, 스레드 간 잠재적 공유를 신뢰성 있게 판별할 수 있습니다.
일반적으로 메모리 관리에서는 근본적인 선택이 필요합니다:
Perceus를 통해 이 간극을 줄이고, 목표는 C/C++ 성능의 2배 이내에 들어가는 것입니다. 초기 벤치마크는 고무적이며, 다양한 메모리 집약 벤치마크에서 Koka가 C 성능에 근접함을 보여줍니다.
Perceus는 참조 카운팅 분석의 일부로 _재사용 분석_도 수행합니다. 이는 패턴 매치를 같은 크기의 생성자와 짝지어 가능하면 이를 _in-place_로 재사용합니다. 예를 들어 리스트에 대한 map 함수는 다음과 같습니다:
fun map( xs : list<a>, f : a -> e b ) : e list<b> match xs Cons(x,xx) -> Cons( f(x), map(xx,f) ) Nil -> Nil fun map( xs : list std/core/types/list: V -> V<a>, f : a -> e b ) : e list std/core/types/list: V -> V<b> match xs Cons std/core/types/Cons: forall<a> (head : a, tail : list<a>) -> list<a>(x,xx) -> Cons std/core/types/Cons: forall<a> (head : a, tail : list<a>) -> list<a>( f(x), map(xx,f) ) Nil std/core/types/Nil: forall<a> list<a> -> Nil std/core/types/Nil: forall<a> list<a>
여기서 매치된 Consstd/core/types/Cons: forall<a> (head : a, tail : list<a>) -> list<a>는 분기에서 새로 만드는 Consstd/core/types/Cons: forall<a> (head : a, tail : list<a>) -> list<a>에 의해 재사용될 수 있습니다. 이는 list(1,100000).map(sqr).sumstd/core/list/sum: (xs : list<int>) -> int처럼 공유되지 않은 리스트를 매핑할 경우, 추가 할당 없이 리스트가 _in-place_로 업데이트됨을 의미합니다. 이는 많은 함수형 스타일 프로그램에서 매우 효과적입니다.
void map( list_t xs, function_t f,
list_t* res)
{
while (is_Cons(xs)) {
if (is_unique(xs)) { // if xs is not shared..
box_t y = apply(dup(f),xs->head);
if (yielding()) { ... } // if f yields to a general ctl operation..
else {
xs->head = y;
*res = xs; // update previous node in-place
res = &xs->tail; // set the result address for the next node
xs = xs->tail; // .. and continue with the next node
}
}
else { ... } // slow path allocates fresh nodes
}
*res = Nil;
}
또한 Koka 컴파일러는 tail-recursion modulo cons (TRMC)[11, 12]도 구현하며, 재귀 호출을 사용하는 대신 결국에는 오른쪽의 C 코드 예시와 유사한 빠른 경로의 in-place 업데이트 루프로 최적화됩니다.
중요한 점은, 재사용 최적화가 보장되며 프로그래머가 언제 최적화가 적용되는지 알 수 있다는 것입니다. 이는 우리가 FBIP라고 부르는 새로운 프로그래밍 기법으로 이어집니다: functional but in-place. 꼬리 재귀가 일반 함수 호출로 루프를 표현하게 해주듯, 재사용 분석은 많은 명령형 알고리즘을 순수 함수형 스타일로 표현하게 해줍니다.
일반화된 tail-recursion modulo cons 논문 읽기
Perceus의 효과와 Koka 코어 언어의 강한 의미론을 보여주는 또 다른 예로, red-black tree 예제에서 이진 트리를 fold할 때 생성되는 코드를 볼 수 있습니다. 레드-블랙 트리는 다음과 같이 정의됩니다:
type color Red Black type tree<k,a> Leaf Node(color : color, left : tree<k,a>, key : k, value : a, right : tree<k,a>) type color why/color: V Red why/Red: color Black why/Black: colortype tree why/tree: (V, V) -> V<k k: V,a a: V> Leaf why/Leaf: forall<a,b> tree<a,b> Node why/Node: forall<a,b> (color : color, left : tree<a,b>, key : a, value : b, right : tree<a,b>) -> tree<a,b>(color : color why/color: V, left : tree why/tree: (V, V) -> V<k k: V,a a: V>, key : k k: V, value : a a: V, right : tree why/tree: (V, V) -> V<k k: V,a a: V>)
트리 t를 함수 f로 일반적으로 fold하는 것은 다음과 같습니다:
fun fold(t : tree<k,a>, acc : b, f : (k, a, b) -> b) : b match t Node(,l,k,v,r) -> r.fold( f(k,v,l.fold(acc,f)), f) Leaf -> acc fun fold why/fold: forall<a,b,c> (t : tree<c,a>, acc : b, f : (c, a, b) -> b) -> b(t t: tree<$616,$614> : tree why/tree: (V, V) -> V<k k: V,a a: V>, acc acc: $615 : b b: V, f f: ($616, $614, $615) -> $615 : (k k: V, a a: V, b b: V) -> b std/core/types/total: E)result: -> total 715 : b std/core/types/total: E match t t: tree<$616,$614> Node why/Node: forall<a,b> (color : color, left : tree<a,b>, key : a, value : b, right : tree<a,b>) -> tree<a,b>(,l l: tree<$616,$614>,k k: $616,v v: $614,r r: tree<$616,$614>) -> r r: tree<$616,$614>.fold why/fold: (t : tree<$616,$614>, acc : $615, f : ($616, $614, $615) -> $615) -> $615( f f: ($616, $614, $615) -> $615(k k: $616,v v: $614,l l: tree<$616,$614>.fold why/fold: (t : tree<$616,$614>, acc : $615, f : ($616, $614, $615) -> $615) -> $615(acc acc: $615,f f: ($616, $614, $615) -> $615)), f f: ($616, $614, $615) -> $615) Leaf why/Leaf: forall<a,b> tree<a,b> -> acc acc: $615
이것은 예제에서 t : treewhy/tree: (V, V) -> V<k,boolstd/core/types/bool: V> 트리에 있는 모든 Truestd/core/types/True: bool 값을 세는 데 사용됩니다:
val count = t.fold(0, fn(k,v,acc) if v then acc+1 else acc) val count = t.fold(0, fn(k,v,acc) if v then acc+1 else acc)
이는 임의 정밀도 정수 산술을 사용하는 다형 일급 함수를 넘기기 때문에 비용이 커 보일 수 있습니다. 하지만 Koka 컴파일러는 먼저 전달된 함수에 맞춰 fold 정의를 _특수화_하고, 결과 단형 코드를 단순화한 다음, Perceus를 적용해 참조 카운트 명령을 삽입합니다. 그 결과 내부 코어 코드는 다음과 같습니다:
fun spec-fold(t : tree<k,bool>, acc : int) : int match t Node(,l,k,v,r) -> if unique(t) then { drop(k); free(t) } else { dup(l); dup(r) } // perceus inserted val x = if v then 1 else 0 spec-fold(r, spec-fold(l,acc) + x) Leaf -> drop(t) acc val count = spec-fold(t,0) fun spec-fold(t : tree why/tree: (V, V) -> V<k,bool std/core/types/bool: V>, acc : int std/core/types/int: V) : int std/core/types/int: V match t Node why/Node: forall<a,b> (color : color, left : tree<a,b>, key : a, value : b, right : tree<a,b>) -> tree<a,b>(,l,k,v,r) -> if unique std/core/unique: () -> ndet int(t) then { drop(k); free(t) } else { dup(l); dup(r) } val x = if v then 1 else 0 spec-fold(r, spec-fold(l,acc) + x) Leaf why/Leaf: forall<a,b> tree<a,b> -> drop(t) acc val count = spec-fold(t,0)
C 백엔드를 통해 컴파일되면 arm64에서 생성되는 어셈블리 명령은 다음과 같습니다:
spec_fold:
...
LOOP0:
mov x21, x0 ; x20 is t, x21 = acc (x19 = koka context _ctx)
LOOP1: ; the "match(t)" point
cmp x20, #9 ; is t a Leaf?
b.eq LBB15_1 ; if so, goto Leaf brach
LBB15_5: ; otherwise, this is the Node(_,l,k,v,r) branch
mov x23, x20 ; load the fields of t:
ldp x22, x0, [x20, #8] ; x22 = l, x0 = k (ldp == load pair)
ldp x24, x20, [x20, #24] ; x24 = v, x20 = r
ldr w8, [x23, #4] ; w8 = reference count (0 is unique)
cbnz w8, LBB15_11 ; if t is not unique, goto cold path to dup the members
tbz w0, #0, LBB15_13 ; if k is allocated (bit 0 is 0), goto cold path to free it
LBB15_7:
mov x0, x23 ; call free(t)
bl _mi_free
LBB15_8:
mov x0, x22 ; call spec_fold(l,acc,_ctx)
mov x1, x21
mov x2, x19
bl spec_fold
cmp x24, #1 ; boxed value is False?
b.eq LOOP0 ; if v is False, the result in x0 is the accumulator
add x21, x0, #4 ; otherwise add 1 (as a small int 4*n)
orr x8, x21, #1 ; check for bigint or overflow in one test
cmp x8, w21, sxtw ; (see kklib/include/integer.h for details)
b.eq LOOP1 ; and tail-call into spec_fold if no overflow or bigint
mov w1, #5 ; otherwise, use generic bigint addition
mov x2, x19
bl _kk_integer_add_generic
b LOOP0
...
고차 매개변수를 가진 다형 fold는 결국 거의 최적의 어셈블리 명령을 가진 촘촘한 루프로 컴파일됩니다.
advanced 여기서도 노드 t가 더 이상 살아있지 않게 되는 즉시 명시적으로 해제되는 것을 볼 수 있습니다. 이는 보통 스코프 기반 해제(RAII 등)보다 더 이르며, 따라서 Perceus는 (사이클 없는) 프로그램에서 객체가 도달 불가능해지는 즉시 항상 바로 해제되는 _garbage-free_를 보장할 수 있습니다[13–15, 22]. 또한 이는 결정적이며 일반적인 malloc/free 호출처럼 동작합니다. 참조 카운팅은 추적 기반 가비지 컬렉션에 비해 비용이 커 보일 수 있는데, 후자는 살아있는 객체를 (재)방문만 하고 객체를 명시적으로 해제할 필요가 없기 때문입니다. 하지만 Perceus는 보통(예제처럼) 마지막 사용 직후 객체를 해제하므로, 메모리가 캐시에 남아 있어 해제 비용이 줄어듭니다. 또한 Perceus는 임의로 살아있는 객체를 (재)방문하지 않으므로, 특히 라이브 셋이 큰 경우 캐시를 망치는 일이 없습니다. 따라서 우리는 Perceus의 결정적 동작과 garbage-free 성질이 실제로 더 잘 작동할 수 있다고 생각합니다.
garbage-free 및 frame-limited reuse 기술 보고서 읽기
완전 in-place 함수형 프로그래밍 기술 보고서 읽기
이 문서는 Koka 프로그래밍 언어에 대한 짧은 소개입니다.
Koka는 함수 중심 언어로, 순수 값과 부작용을 동반하는 계산을 분리합니다(효과 타이핑의 중요성 때문에, Koka라는 이름은 일본어로 _effective_를 의미하는 단어(効果, こうか, Kōka)에서 유래했습니다).
보통처럼 익숙한 Hello world 프로그램부터 시작합니다:
fun main() println("Hello world!") // println output fun main tour/main: () -> console ()()result: -> console () println std/core/console/string/println: (s : string) -> console ()("Hello world!"literal: string count= 12)
함수는 fun 키워드로 선언하며(익명 함수는 fn), brace elision 때문에 들여쓰기된 블록은 암묵적으로 중괄호가 붙습니다. 따라서 예제는 다음처럼도 쓸 수 있습니다:
fun main() { println("Hello world!") // println output } fun main tour/main: () -> console ()() { println("Hello world!") }
명시적 중괄호를 사용한 버전입니다. 다음은 _카이사르 암호_로 문자열을 인코딩하는 짧은 예제로, 문자열의 각 소문자 글자를 알파벳에서 세 글자 뒤의 문자로 바꿉니다:
fun main() { println(caesar("koka is fun")) } fun encode( s : string, shift : int ) fun encode-char(c) if c < 'a' || c > 'z' then return c val base = (c - 'a').int val rot = (base + shift) % 26 (rot.char + 'a') s.map(encode-char) fun caesar( s : string ) : string s.encode( 3 ) fun encode tour/encode: (s : string, shift : int) -> string( s s: string : string std/core/types/string: V, shift shift: int : int std/core/types/int: V )result: -> total string fun encode-char encode-char: (c : char) -> char(c c: char)result: -> total char if c c: char <std/core/char/(<): (char, char) -> bool 'a'literal: char unicode= u0061 ||std/core/types/(||): (x : bool, y : bool) -> bool c c: char >std/core/char/(>): (char, char) -> bool 'z'literal: char unicode= u007A then return return: char c c: char val base base: int = (c c: char -std/core/char/(-): (c : char, d : char) -> char 'a'literal: char unicode= u0061).int std/core/char/int: (char) -> int val rot rot: int = (base base: int +std/core/int/(+): (x : int, y : int) -> int shift shift: int) %std/core/int/(%): (int, int) -> int 26 literal: int dec = 26 hex8 = 0x1A bit8 = 0b00011010 (rot rot: int.char std/core/char/int/char: (i : int) -> char +std/core/char/(+): (c : char, d : char) -> char 'a'literal: char unicode= u0061) s s: string.map std/core/list/string/map: (s : string, f : (char) -> char) -> string(encode-char encode-char: (c : char) -> char) fun caesar tour/caesar: (s : string) -> string( s s: string : string std/core/types/string: V )result: -> total string : string std/core/types/string: V s s: string.encode tour/encode: (s : string, shift : int) -> string( 3 literal: int dec = 3 hex8 = 0x03 bit8 = 0b00000011 )
이 예제에서 하나의 문자 c를 인코딩하는 로컬 함수 encode-char를 선언합니다. 마지막 문장 s.map(encode-char)는 문자열 s의 각 문자에 encode-char 함수를 적용하여, 각 문자가 카이사르 인코딩된 새로운 문자열을 반환합니다. 함수의 마지막 문장 결과는 그 함수의 반환 값이기도 하므로, 일반적으로 명시적인 return 키워드를 생략할 수 있습니다.
Koka는 함수 중심 언어로, 언어의 핵심은 _함수_와 _데이터_입니다(예를 들어 객체와 대조적으로). 특히 식 s.encode(3)는 stringstd/core/types/string: V 객체에서 encode 메서드를 선택하는 것이 아니라, 단지 함수 호출 encode(s,3)의 설탕 문법으로 s가 첫 번째 인자가 됩니다. 마찬가지로 c.int는 int(c)를 호출하여 문자를 정수로 변환하며(두 표현은 동등합니다). 점 표기는 직관적이고 여러 호출을 체이닝하는 데 매우 편리합니다. 예를 들어:
fun showit( s : string ) s.encode(3).count.println fun showit tour/showit: (s : string) -> console ()( s s: string : string std/core/types/string: V )result: -> console () s s: string.encode tour/encode: (s : string, shift : int) -> console string(3 literal: int dec = 3 hex8 = 0x03 bit8 = 0b00000011).count std/core/string/chars/count: (s : string) -> console int.println std/core/console/default/show/println: (x : int, @implicit/show : (int) -> console string) -> console () ?show=int/show
(본문은 println(count(encode(s,3)))로 디슈가됩니다). 점 표기가 함수 호출의 설탕 문법이라는 장점은, 어떤 데이터 타입의 ‘기본’ 메서드도 쉽게 확장할 수 있다는 점입니다: 첫 번째 인자로 그 타입을 받는 새 함수를 작성하면 됩니다. 대부분의 객체 지향 언어에서는 클래스 정의 자체에 메서드를 추가해야 하는데, 예를 들어 해당 클래스가 라이브러리로 제공되는 경우 이는 항상 가능하지 않습니다.
Koka는 또한 강타입 언어입니다. Koka는 강력한 타입 추론 엔진을 사용해 대부분의 타입을 추론하며, 타입은 대개 방해가 되지 않습니다. 특히 로컬 변수의 타입은 항상 생략할 수 있습니다. 예를 들어 이전 예제의 base와 rot 값이 그렇습니다. 예제 위에 마우스를 올려 Koka가 추론한 타입을 확인해보세요. 일반적으로는 함수 매개변수와 함수 결과에 타입 주석을 쓰는 것이 좋은 관행인데, 타입 추론에도 도움이 되고 컴파일러로부터 더 나은 피드백을 받으며 유용한 문서가 되기 때문입니다.
encode 함수에서는 s 매개변수의 타입을 주는 것이 실제로 필수입니다. map 함수가 liststd/core/types/list: V -> V와 stringstd/core/types/string: V 모두에 대해 정의되어 있어, 주석이 없으면 프로그램이 모호해지기 때문입니다. 편집기에서 예제를 로드한 뒤 주석을 제거해보면 Koka가 어떤 오류를 내는지 볼 수 있습니다.
Koka는 fn 키워드를 사용한 익명 함수 표현식도 허용합니다. 예를 들어 encode-char 함수를 선언하는 대신, 함수 표현식을 map에 직접 전달할 수도 있습니다:
fun encode2( s : string, shift : int ) s.map( fn(c) if c < 'a' || c > 'z' then return c val base = (c - 'a').int val rot = (base + shift) % 26 (rot.char + 'a') ) fun encode2 tour/encode2: (s : string, shift : int) -> string( s s: string : string std/core/types/string: V, shift shift: int : int std/core/types/int: V )result: -> total string s s: string.map std/core/list/string/map: (s : string, f : (char) -> char) -> string( fn fn: (c : char) -> char(c c: char) if c c: char <std/core/char/(<): (char, char) -> bool 'a'literal: char unicode= u0061 ||std/core/types/(||): (x : bool, y : bool) -> bool c c: char >std/core/char/(>): (char, char) -> bool 'z'literal: char unicode= u007A then return return: char c c: char val base base: int = (c c: char -std/core/char/(-): (c : char, d : char) -> char 'a'literal: char unicode= u0061).int std/core/char/int: (char) -> int val rot rot: int = (base base: int +std/core/int/(+): (x : int, y : int) -> int shift shift: int) %std/core/int/(%): (int, int) -> int 26 literal: int dec = 26 hex8 = 0x1A bit8 = 0b00011010 (rot rot: int.char std/core/char/int/char: (i : int) -> char +std/core/char/(+): (c : char, d : char) -> char 'a'literal: char unicode= u0061) )
이전 예제에서는 마지막 중괄호 뒤에 닫는 괄호를 두어야 하는 것이 약간 번거롭습니다. 편의 기능으로, Koka는 익명 함수가 함수 호출 뒤에 올 수 있도록 허용합니다 — 이는 _trailing lambdas_로도 알려져 있습니다. 예를 들어 1부터 10까지 숫자를 출력하는 방법은 다음과 같습니다:
fun main() { print10() } fun print10() for(1,10) fn(i) println(i) fun print10 tour/print10: () -> console ()()result: -> console () for std/core/range/for: (start : int, end : int, action : (int) -> console ()) -> console ()(1 literal: int dec = 1 hex8 = 0x01 bit8 = 0b00000001,10 literal: int dec = 10 hex8 = 0x0A bit8 = 0b00001010) fn fn: (i : int) -> console ()(i i: int) println std/core/console/default/show/println: (x : int, @implicit/show : (int) -> console string) -> console () ?show=int/show(i i: int)
이는 for( 1, 10, fn(i){ println(i) } )로 디슈가됩니다. (사실 println에 i 인자를 그대로 넘기므로 for(1,10,println)처럼 함수 자체를 직접 넘길 수도 있습니다.)
인자가 없는 익명 함수는 fn 키워드도 생략하고 중괄호만 사용하는 방식으로 더 줄일 수 있습니다. 다음은 repeat 함수를 사용하는 예입니다:
fun main() { printhi10() } fun printhi10() repeat(10) println("hi") fun printhi10 tour/printhi10: () -> console ()()result: -> console () repeat std/core/repeat: (n : int, action : () -> console ()) -> console ()(10 literal: int dec = 10 hex8 = 0x0A bit8 = 0b00001010) println std/core/console/string/println: (s : string) -> console ()("hi"literal: string count= 2)
여기서 본문은 repeat( 10, { println(``histd/num/int32/hi: (i : int32) -> int32``) } )로 디슈가되며, 이는 다시 repeat( 10, fn(){ println(``histd/num/int32/hi: (i : int32) -> int32``)} )로 디슈가됩니다. 이는 특히 whilestd/core/while: forall<e> (predicate : () -> <div|e> bool, action : () -> <div|e> ()) -> <div|e> () 루프에 편리한데, 이는 내장 제어 흐름 구문이 아니라 일반 함수이기 때문입니다:
fun main() { print11() } fun print11() var i := 10 while { i >= 0 } println(i) i := i - 1 fun print11 tour/print11: () -> <console,div> ()()result: -> <console,div> () var i i: local-var<$1267,int> := 10 literal: int dec = 10 hex8 = 0x0A bit8 = 0b00001010 while std/core/while: (predicate : () -> <div,local<$1267>,console> bool, action : () -> <div,local<$1267>,console> ()) -> <div,local<$1267>,console> () { i i: int ?hdiv=iev@1292 >=std/core/int/(>=): (x : int, y : int) -> <local<$1267>,div,console> bool 0 literal: int dec = 0 hex8 = 0x00 bit8 = 0b00000000 } println std/core/console/default/show/println: (x : int, @implicit/show : (int) -> <console,local<$1267>,div> string) -> <console,local<$1267>,div> () ?show=int/show(i i: int ?hdiv=iev@1373) i i: local-var<$1267,int> :=std/core/types/local-set: (v : local-var<$1267,int>, assigned : int) -> <local<$1267>,console,div> () i i: int ?hdiv=iev@1445 -std/core/int/(-): (x : int, y : int) -> <local<$1267>,console,div> int 1 literal: int dec = 1 hex8 = 0x01 bit8 = 0b00000001
whilestd/core/while: forall<e> (predicate : () -> <div|e> bool, action : () -> <div|e> ()) -> <div|e> ()의 첫 번째 인자가 보통의 괄호가 아니라 중괄호라는 점에 주목하세요. Koka에서는 괄호 안의 식은 함수 호출 전에 항상 평가되는 반면, 중괄호 안의 식(아, 멜빵!)은 지연되어 평가되지 않을 수도 있고 여러 번 평가될 수도 있습니다(예제처럼).
우리가 아는 한, Koka는 일반화된 _trailing lambdas_를 가진 첫 번째 언어였습니다. 또한 _dot notation_을 가진 초기 언어 중 하나이기도 합니다(이는 독립적으로 개발되었지만 D 언어에도 비슷한 기능(UFCS)이 있으며 dot-notation보다 먼저 나왔습니다). 또 다른 새로운 문법 기능은 with 문입니다. 함수 블록을 인자로 넘기기 쉬워지면 이런 것들이 종종 중첩됩니다. 예를 들어:
fun twice(f) f() f() fun test-twice() twice twice println("hi") fun twice tour/twice: forall<a,e> (f : () -> e a) -> e a(f f: () -> _1473 _1474)result: -> 1484 1483 f f: () -> _1473 _1474() f f: () -> _1473 _1474() fun test-twice tour/test-twice: () -> console ()()result: -> console () twice tour/twice: (f : () -> console ()) -> console () twice tour/twice: (f : () -> console ()) -> console () println std/core/console/string/println: (s : string) -> console ()("hi"literal: string count= 2)
여기서 "hi"는 네 번 출력됩니다(참고: 이는 twicetour/twice: forall<a,e> (f : () -> e a) -> e a( fn(){ twicetour/twice: forall<a,e> (f : () -> e a) -> e a( fn(){ println("hi") }) })로 디슈가됩니다). with 문을 사용하면 다음처럼 더 간결하게 쓸 수 있습니다:
pub fun test-with1() with twice with twice println("hi") pub fun test-with1 tour/test-with1: () -> console ()()result: -> console () with with: () -> console () twice tour/twice: (f : () -> console ()) -> console () with with: () -> console () twice tour/twice: (f : () -> console ()) -> console () println std/core/console/string/println: (s : string) -> console ()("hi"literal: string count= 2)
with 문은 본질적으로 그 뒤에 오는 모든 문장을 익명 함수 블록에 넣고, 이를 마지막 매개변수로 전달합니다. 일반적으로:
translation
with f(e1,...,eN) <body>with f(e1,...,eN) <body>f(e1,...,eN, fn(){ <body> }) f(e1,...,eN, fn(){ <body> })
또한 with 문은 다음처럼 변수 매개변수를 바인딩할 수도 있습니다:
translation
with x <- f(e1,...,eN) <body>with x <- f(e1,...,eN) <body>f(e1,...,eN, fn(x){ <body> }) f(e1,...,eN, fn(x){ <body> })
다음은 foreach를 사용해 함수 본문의 나머지 범위를 순회하는 예입니다:
pub fun test-with2() { with x <- list(1,10).foreach println(x) } pub fun test-with2 tour/test-with2: () -> console ()()result: -> console () { with with: (x : int) -> console () x x: int <- list std/core/list/range/list: (lo : int, hi : int) -> console list<int>(1 literal: int dec = 1 hex8 = 0x01 bit8 = 0b00000001,10 literal: int dec = 10 hex8 = 0x0A bit8 = 0b00001010).foreach std/core/list/foreach: (xs : list<int>, action : (int) -> console ()) -> console () println std/core/console/default/show/println: (x : int, @implicit/show : (int) -> console string) -> console () ?show=int/show(x x: int) }
이는 list(1,10).foreach( fn(x){ println(x) } )로 디슈가됩니다. 이는 Haskell의 do 표기법을 약간 떠올리게 합니다. 이런 식의 with 사용은 처음에는 조금 이상해 보일 수 있지만, 실제로는 매우 편리합니다 — with를 어휘 스코프의 나머지에 대한 클로저로 생각하는 데 도움이 됩니다.
또 다른 예로, finallystd/core/hnd/finally: forall<a,e> (fin : () -> e (), action : () -> e a) -> e a 함수는 스코프를 빠져나갈 때(정상 종료든 “예외”(즉 효과 연산이 재개하지 않는 경우)든) 실행되는 함수를 첫 번째 인자로 받습니다. 여기서도 with는 자연스럽게 들어맞습니다:
fun test-finally() with finally{ println("exiting..") } println("entering..") throw("oops") + 42 fun test-finally tour/test-finally: () -> <console,exn> int()result: -> <console,exn> int with with: () -> <console,exn> int finally std/core/hnd/finally: (fin : () -> <console,exn> (), action : () -> <console,exn> int) -> <console,exn> int{ println std/core/console/string/println: (s : string) -> <console,exn> ()("exiting.."literal: string count= 9) } println std/core/console/string/println: (s : string) -> <console,exn> ()("entering.."literal: string count= 10) throw std/core/exn/throw: (message : string, info : ? exception-info) -> <exn,console> int("oops"literal: string count= 4) +std/core/int/(+): (x : int, y : int) -> <exn,console> int 42 literal: int dec = 42 hex8 = 0x2A bit8 = 0b00101010
이는 finallystd/core/hnd/finally: forall<a,e> (fin : () -> e (), action : () -> e a) -> e a(fn(){ println(...) }, fn(){ println("entering"); throwstd/core/exn/throw: forall<a> (message : string, info : ? exception-info) -> exn a("oops") + 42 })로 디슈가되며, 다음을 출력합니다:
entering..
exiting..
uncaught exception: oops
이는 min-gen 원리의 또 다른 예입니다. 많은 언어가 defer 같은 패턴을 위한 특별한 내장 지원을 갖지만, Koka에서는 모두 최소한의 문법적 설탕을 가진 함수 적용일 뿐입니다.
with 문은 효과 핸들러와 결합할 때 특히 유용합니다. 효과는 추상적 연산들의 집합을 설명하며, 구체적 구현은 핸들러에 의해 동적으로 바인딩될 수 있습니다. 다음은 메시지를 방출(emit)하는 효과 핸들러의 예입니다:
// declare an abstract operation: emit, how it emits is defined dynamically by a handler. effect fun emit(msg : string) : () // emit a standard greeting. fun hello() : emit () emit("hello world!") // emit a standard greeting to the console. pub fun hello-console1() : console () with handler fun emit(msg) println(msg) hello() effect tour/emit: (E, V) -> V fun emit tour/emit: (E, V) -> V(msg msg: string : string std/core/types/string: V) : (std/core/types/unit: V)std/core/types/unit: Vfun hello tour/hello: () -> emit ()()result: -> emit () : emit tour/emit: (E, V) -> V (std/core/types/unit: V)std/core/types/unit: V emit tour/emit: (msg : string) -> emit ()("hello world!"literal: string count= 12) pub fun hello-console1 tour/hello-console1: () -> console ()()result: -> console () : console std/core/console/console: X (std/core/types/unit: V)std/core/types/unit: V with with: () -> <emit,console> () handler handler: (() -> <emit,console> ()) -> console () fun emit emit: (msg : string) -> console ()(msg msg: string) println std/core/console/string/println: (s : string) -> console ()(msg msg: string) hello tour/hello: () -> <emit,console> ()()
이 예제에서 with 식은 (handler{ fun emittour/emit: (msg : string) -> emit ()(msg){ println(msg) } })( fn(){ hellotour/hello: () -> emit ()() } )로 디슈가됩니다. 일반적으로 handler{ <ops> } 식은 마지막 인자로 함수 블록을 받으므로 with와 함께 바로 사용할 수 있습니다.
또한, 한 가지 연산만 정의하는 효과(예: emittour/emit: (E, V) -> V)에 대해서는 편의상 handler 키워드를 생략할 수 있습니다:
translation
with val op = <expr> with fun op(x){ <body> } with ctl op(x){ <body> } with val op = <expr>with fun op(x){ <body> } with ctl op(x){ <body> } with handler{ val op = <expr> } with handler{ fun op(x){ <body> } } with handler{ ctl op(x){ <body> } } with handler{ val op = <expr> } with handler{ fun op(x){ <body> } } with handler{ ctl op(x){ <body> } }
이 편의 기능을 사용하면 이전 예제를 다음처럼 더 간결하게 쓸 수 있습니다:
pub fun hello-console2() with fun emit(msg) println(msg) hello() pub fun hello-console2() with fun emit tour/emit: (msg : string) -> emit ()(msg) println(msg) hello tour/hello: () -> emit ()()
직관적으로 with fun emittour/emit: (msg : string) -> emit () 핸들러는, 나머지 스코프에 대해 emittour/emit: (msg : string) -> emit () 함수를 (정적으로 타입이 붙은) 동적 바인딩으로 보는 것이 가능합니다.
함수 중심 언어답게, Koka는 선택적/이름 붙은 매개변수를 모두 지원하는 강력한 함수 호출을 제공합니다. 예를 들어 replace-allstd/core/string/replace-all: (s : string, pattern : string, repl : string) -> string 함수는 문자열, 패턴(이름 pattern), 그리고 치환 문자열(이름 repl)을 받습니다:
fun main() { println(world()) } fun world() replace-all("hi there", "there", "world") // returns "hi world" fun world tour/world: () -> string()result: -> total string replace-all std/core/string/replace-all: (s : string, pattern : string, repl : string) -> string("hi there"literal: string count= 8, "there"literal: string count= 5, "world"literal: string count= 5)
이름 붙은 매개변수를 사용하면 호출을 다음처럼 쓸 수도 있습니다:
fun main() { println(world2()) } fun world2() "hi there".replace-all( repl="world", pattern="there" ) fun world2 tour/world2: () -> string()result: -> total string "hi there"literal: string count= 8.replace-all std/core/string/replace-all: (s : string, pattern : string, repl : string) -> string( repl="world"literal: string count= 5, pattern="there"literal: string count= 5 )
선택적 매개변수는 호출 지점에서 반드시 제공할 필요가 없는 매개변수에 기본 값을 지정할 수 있게 합니다. 예를 들어 리스트, start 위치, 그리고 부분 리스트 길이 len을 받는 sublisttour/sublist: forall<a> (xs : list<a>, start : int, len : ? int) -> list<a> 함수를 정의해봅시다. len을 선택적으로 만들고, 기본으로는 start 뒤의 모든 요소를 반환하도록 입력 리스트의 길이를 기본값으로 선택할 수 있습니다:
fun main() { println( ['a','b','c'].sublist(1).string ) } fun sublist( xs : list<a>, start : int, len : int = xs.length ) : list<a> if start <= 0 return xs.take(len) match xs Nil -> Nil Cons(,xx) -> xx.sublist(start - 1, len) fun sublist tour/sublist: forall<a> (xs : list<a>, start : int, len : ? int) -> list<a>( xs xs: list<$2173> : list std/core/types/list: V -> V<a a: V>, start start: int : int std/core/types/int: V, len len: ? int : int std/core/types/int: V = xs xs: list<$2173>.length std/core/list/length: (xs : list<$2173>) -> int )result: -> total list<2278> : list std/core/types/list: V -> V<a a: V> if start start: int <=std/core/int/(<=): (x : int, y : int) -> bool 0 literal: int dec = 0 hex8 = 0x00 bit8 = 0b00000000 return return: list<$2173> xs xs: list<$2173>.take std/core/list/take: (xs : list<$2173>, n : int) -> list<$2173>(len len: int)std/core/types/Unit: () match xs xs: list<$2173> Nil std/core/types/Nil: forall<a> list<a> -> Nil std/core/types/Nil: forall<a> list<a> Cons std/core/types/Cons: forall<a> (head : a, tail : list<a>) -> list<a>(,xx xx: list<$2173>) -> xx xx: list<$2173>.sublist tour/sublist: (xs : list<$2173>, start : int, len : ? int) -> list<$2173>(start start: int -std/core/int/(-): (x : int, y : int) -> int 1 literal: int dec = 1 hex8 = 0x01 bit8 = 0b00000001, len len: int)
sublisttour/sublist: forall<a> (xs : list<a>, start : int, len : ? int) -> list<a> 식별자 위에 마우스를 올리면 전체 타입을 볼 수 있으며, 여기서 len 매개변수는 물음표로 표시되는 선택적 intstd/core/types/int: V 타입을 갖습니다: ? intstd/core/types/int: V.
여기에는 Graham Hutton의 훌륭한 책 “Programming in Haskell”의 예제에서 영감을 얻은 조금 더 큰 프로그램이 있습니다:
import std/num/float64 import std/num/float64 std/num/float64fun main() test-uncaesar() fun encode( s : string, shift : int ) fun encode-char(c) if c < 'a' || c > 'z' return c val base = (c - 'a').int val rot = (base + shift) % 26 (rot.char + 'a') s.map(encode-char) // The letter frequency table for English val english = [8.2,1.5,2.8,4.3,12.7,2.2, 2.0,6.1,7.0,0.2,0.8,4.0,2.4, 6.7,7.5,1.9,0.1, 6.0,6.3,9.1, 2.8,1.0,2.4,0.2,2.0,0.1] // Small helper functions fun percent( n : int, m : int ) 100.0 * (n.float64 / m.float64) fun rotate( xs : list<a>, n : int ) : list<a> xs.drop(n) ++ xs.take(n) // Calculate a frequency table for a string fun freqs( s : string ) : list<float64> val lowers = list('a','z') val occurs = lowers.map( fn(c) s.count(c.string) ) val total = occurs.sum occurs.map( fn(i) percent(i,total) ) // Calculate how well two frequency tables match according // to the chi-square statistic. fun chisqr( xs : list<float64>, ys : list<float64> ) : float64 zipwith(xs,ys, fn(x,y) ((x - y)^2.0)/y ).foldr(0.0,(+)) // Crack a Caesar encoded string fun uncaesar( s : string ) : string val table = freqs(s) // build a frequency table for s val chitab = list(0,25).map fn(n) // build a list of chisqr numbers for each shift between 0 and 25 chisqr( table.rotate(n), english ) val min = chitab.minimum() // find the mininal element val shift = chitab.index-of( fn(f) f == min ).negate // and use its position as our shift s.encode( shift ) fun test-uncaesar() println( uncaesar( "nrnd lv d ixq odqjxdjh" ) ) val english tour/english: list<float64> = [std/core/types/Cons: forall<a> (head : a, tail : list<a>) -> list<a>8.2 literal: float64 hex64= 0x1.0666666666666p3,1.5 literal: float64 hex64= 0x1.8p0,2.8 literal: float64 hex64= 0x1.6666666666666p1,4.3 literal: float64 hex64= 0x1.1333333333333p2,12.7 literal: float64 hex64= 0x1.9666666666666p3,2.2 literal: float64 hex64= 0x1.199999999999ap1, 2.0 literal: float64 hex64= 0x1p1,6.1 literal: float64 hex64= 0x1.8666666666666p2,7.0 literal: float64 hex64= 0x1.cp2,0.2 literal: float64 hex64= 0x1.999999999999ap-3,0.8 literal: float64 hex64= 0x1.999999999999ap-1,4.0 literal: float64 hex64= 0x1p2,2.4 literal: float64 hex64= 0x1.3333333333333p1, 6.7 literal: float64 hex64= 0x1.acccccccccccdp2,7.5 literal: float64 hex64= 0x1.ep2,1.9 literal: float64 hex64= 0x1.e666666666666p0,0.1 literal: float64 hex64= 0x1.999999999999ap-4, 6.0 literal: float64 hex64= 0x1.8p2,6.3 literal: float64 hex64= 0x1.9333333333333p2,9.1 literal: float64 hex64= 0x1.2333333333333p3, 2.8 literal: float64 hex64= 0x1.6666666666666p1,1.0 literal: float64 hex64= 0x1p0,2.4 literal: float64 hex64= 0x1.3333333333333p1,0.2 literal: float64 hex64= 0x1.999999999999ap-3,2.0 literal: float64 hex64= 0x1p1,0.1 literal: float64 hex64= 0x1.999999999999ap-4[]std/core/types/Nil: forall<a> list<a>](https://koka-lang.github.io/koka/doc/std_core_types.html#con_space_Nil)fun percent tour/percent: (n : int, m : int) -> float64( n n: int : int std/core/types/int: V, m m: int : int std/core/types/int: V )result: -> total float64 100.0 literal: float64 hex64= 0x1.9p6 std/num/float64/(): (x : float64, y : float64) -> float64 (n n: int.float64 std/num/float64/float64: (i : int) -> float64 /std/num/float64/(/): (x : float64, y : float64) -> float64 m m: int.float64 std/num/float64/float64: (i : int) -> float64) fun rotate tour/rotate: forall<a> (xs : list<a>, n : int) -> list<a>( xs xs: list<$2516> : list std/core/types/list: V -> V<a a: V>, n n: int : int std/core/types/int: V )result: -> total list<2551> : list std/core/types/list: V -> V<a a: V> xs xs: list<$2516>.drop std/core/list/drop: (xs : list<$2516>, n : int) -> list<$2516>(n n: int) ++std/core/list/(++): (xs : list<$2516>, ys : list<$2516>) -> list<$2516> xs xs: list<$2516>.take std/core/list/take: (xs : list<$2516>, n : int) -> list<$2516>(n n: int) fun freqs tour/freqs: (s : string) -> list<float64>( s s: string : string std/core/types/string: V )result: -> total list<float64> : list std/core/types/list: V -> V<float64 std/core/types/float64: V> val lowers lowers: list<char> = list std/core/list/char/list: (lo : char, hi : char) -> list<char>('a'literal: char unicode= u0061,'z'literal: char unicode= u007A) val occurs occurs: list<int> = lowers lowers: list<char>.map std/core/list/map: (xs : list<char>, f : (char) -> int) -> list<int>( fn fn: (c : char) -> int(c c: char) s s: string.count std/core/string/stringpat/count: (s : string, pattern : string) -> int(c c: char.string std/core/string/char/string: (c : char) -> string) ) val total total: int = occurs occurs: list<int>.sum std/core/list/sum: (xs : list<int>) -> int occurs occurs: list<int>.map std/core/list/map: (xs : list<int>, f : (int) -> float64) -> list<float64>( fn fn: (i : int) -> float64(i i: int) percent tour/percent: (n : int, m : int) -> float64(i i: int,total total: int) ) fun chisqr tour/chisqr: (xs : list<float64>, ys : list<float64>) -> float64( xs xs: list<float64> : list std/core/types/list: V -> V<float64 std/core/types/float64: V>, ys ys: list<float64> : list std/core/types/list: V -> V<float64 std/core/types/float64: V> )result: -> total float64 : float64 std/core/types/float64: V zipwith std/core/list/zipwith: (xs : list<float64>, ys : list<float64>, f : (float64, float64) -> float64) -> list<float64>(xs xs: list<float64>,ys ys: list<float64>, fn fn: (x : float64, y : float64) -> float64(x x: float64,y y: float64) ((x x: float64 -std/num/float64/(-): (x : float64, y : float64) -> float64 y y: float64)^std/num/float64/(^): (f : float64, p : float64) -> float642.0 literal: float64 hex64= 0x1p1)/std/num/float64/(/): (x : float64, y : float64) -> float64y y: float64 ).foldr std/core/list/foldr: (xs : list<float64>, z : float64, f : (float64, float64) -> float64) -> float64(0.0 literal: float64 hex64= 0x0p+0,(+)std/num/float64/(+): (x : float64, y : float64) -> float64) fun uncaesar tour/uncaesar: (s : string) -> string( s s: string : string std/core/types/string: V )result: -> total string : string std/core/types/string: V val table table: list<float64> = freqs tour/freqs: (s : string) -> list<float64>(s s: string) val chitab chitab: list<float64> = list std/core/list/range/list: (lo : int, hi : int) -> list<int>(0 literal: int dec = 0 hex8 = 0x00 bit8 = 0b00000000,25 literal: int dec = 25 hex8 = 0x19 bit8 = 0b00011001).map std/core/list/map: (xs : list<int>, f : (int) -> float64) -> list<float64> fn fn: (n : int) -> float64(n n: int) chisqr tour/chisqr: (xs : list<float64>, ys : list<float64>) -> float64( table table: list<float64>.rotate tour/rotate: (xs : list<float64>, n : int) -> list<float64>(n n: int), english tour/english: list<float64> ) val min min: float64 = chitab chitab: list<float64>.minimum std/num/float64/minimum: (xs : list<float64>) -> float64() val shift shift: int = chitab chitab: list<float64>.index-of std/core/list/index-of: (xs : list<float64>, pred : (float64) -> bool) -> int( fn fn: (f : float64) -> bool(f f: float64) f f: float64 ==std/num/float64/(==): (x : float64, y : float64) -> bool min min: float64 ).negate std/core/int/negate: (i : int) -> int s s: string.encode tour/encode: (s : string, shift : int) -> string( shift shift: int ) fun test-uncaesar tour/test-uncaesar: () -> console ()()result: -> console () println std/core/console/string/println: (s : string) -> console ()( uncaesar tour/uncaesar: (s : string) -> console string( "nrnd lv d ixq odqjxdjh"literal: string count= 22 ) )
val 키워드는 정적 값을 선언합니다. 예제에서 englishtour/english: list<float64> 값은 각 문자의 평균 빈도를 나타내는 부동소수점 수(float64std/core/types/float64: V) 리스트입니다. freqstour/freqs: (s : string) -> list<float64> 함수는 특정 문자열에 대한 빈도표를 만들고, chisqrtour/chisqr: (xs : list<float64>, ys : list<float64>) -> float64 함수는 두 빈도표가 얼마나 잘 맞는지 계산합니다. crack 함수에서는 이들을 사용해 englishtour/english: list<float64>와 가장 가깝게 맞는 빈도표를 만드는 shift 값을 찾고, 이를 사용해 문자열을 디코딩합니다. 이 예제는 대화형 환경에서 직접 시험해볼 수 있습니다:
> :l samples/basic/caesar.kk
Koka의 새로운 점은 함수에서 발생하는 모든 _부작용_을 자동으로 추론한다는 것입니다. 어떤 효과도 없음을 totalstd/core/types/total: E(또는 <>)로 나타내며, 이는 순수한 수학 함수에 해당합니다. 함수가 예외를 던질 수 있으면 효과는 exnstd/core/exn/exn: (E, V) -> V이고, 함수가 종료하지 않을 수 있으면 효과는 divstd/core/types/div: X(발산)입니다. exnstd/core/exn/exn: (E, V) -> V와 divstd/core/types/div: X의 조합은 purestd/core/pure: E이며 Haskell의 순수성 개념에 직접 대응합니다. 비결정적 함수는 ndetstd/core/types/ndet: X 효과를 가집니다. 가장 ‘나쁜’ 효과는 iostd/core/io: E로, 예외를 던질 수 있고, 종료하지 않을 수 있으며, 비결정적이고, 힙을 읽고/쓰며, 어떤 입출력도 할 수 있음을 의미합니다. 다음은 효과가 있는 함수들의 예입니다:
fun square1( x : int ) : total int { xx } fun square2( x : int ) : console int { println( "a not so secret side-effect" ); xx } fun square3( x : int ) : div int { x * square3( x ) } fun square4( x : int ) : exn int { throw( "oops" ); x*x } fun square1 tour/square1: (x : int) -> int( x x: int : int std/core/types/int: V )result: -> total int : total std/core/types/total: E int std/core/types/int: V { x x: intstd/core/int/(): (int, int) -> intx x: int } fun square2 tour/square2: (x : int) -> console int( x x: int : int std/core/types/int: V )result: -> console int : console std/core/console/console: X int std/core/types/int: V { println std/core/console/string/println: (s : string) -> console ()( "a not so secret side-effect"literal: string count= 27 ); x x: intstd/core/int/(): (int, int) -> console intx x: int } fun square3 tour/square3: (x : int) -> div int( x x: int : int std/core/types/int: V )result: -> div int : div std/core/types/div: X int std/core/types/int: V { x x: int std/core/int/(): (int, int) -> div int square3 tour/square3: (x : int) -> div int( x x: int ) } fun square4 tour/square4: (x : int) -> exn int( x x: int : int std/core/types/int: V )result: -> exn int : exn std/core/exn/exn: (E, V) -> V int std/core/types/int: V { throw std/core/exn/throw: (message : string, info : ? exception-info) -> exn _3160( "oops"literal: string count= 4 ); x x: intstd/core/int/(): (int, int) -> exn intx x: int }
효과가 totalstd/core/types/total: E인 경우 보통 타입 주석에서 이를 생략합니다. 예를 들어 다음처럼 쓸 때:
fun square5( x : int ) : int x*x fun square5 tour/square5: (x : int) -> int( x x: int : int std/core/types/int: V )result: -> total int : int std/core/types/int: V x x: intstd/core/int/(): (int, int) -> intx x: int
가정되는 효과는 totalstd/core/types/total: E입니다. 때로는 효과가 있는 함수를 작성하지만, 그 효과 타입을 명시적으로 쓰고 싶지 않을 수도 있습니다. 그런 경우 어떤 추론된 타입을 의미하는 _와일드카드 타입_을 사용할 수 있습니다. 와일드카드 타입은 밑줄로 시작하는 식별자 또는 밑줄 단독으로 표기합니다:
fun square6( x : int ) : _e int println("I did not want to write down the "console" effect") x*x fun square6 tour/square6: (x : int) -> console int( x x: int : int std/core/types/int: V )result: -> console int : _e _e: E int std/core/types/int: V println std/core/console/string/println: (s : string) -> console ()("I did not want to write down the "console" effect"literal: string count= 49) x x: intstd/core/int/(): (int, int) -> console intx x: int
square6tour/square6: (x : int) -> console int 위에 마우스를 올리면 _e에 대해 추론된 효과를 볼 수 있습니다.
추론된 효과는 함수에 붙는 단순한 추가 타입 정보로만 취급되지 않습니다. 반대로, 효과 추론을 통해 Koka는 지시적(denotational) 의미론과 매우 강한 연결을 갖습니다. 특히 Koka 함수의 전체 타입은 그 지시적 의미론을 설명하는 수학적 함수의 타입 시그니처에 직접 대응합니다. 예를 들어 타입 t를 해당하는 수학적 타입 시그니처로 번역하기 위해 〚t〛를 사용하면, 다음이 성립합니다:
〚intstd/core/types/int: V -> totalstd/core/types/total: E intstd/core/types/int: V〛=
〚intstd/core/types/int: V -> exnstd/core/exn/exn: (E, V) -> V intstd/core/types/int: V〛=
〚intstd/core/types/int: V -> purestd/core/pure: E intstd/core/types/int: V〛=
〚intstd/core/types/int: V -> <ststd/core/types/st: H -> E<h>,purestd/core/pure: E> intstd/core/types/int: V〛=
위 번역에서 우리는 를 합으로 사용하여 단위 (즉 예외) 또는 타입 둘 중 하나를 갖도록 하고, 를 힙과 타입 의 쌍으로 이루어진 곱으로 사용합니다. 위 대응에서 즉시 볼 수 있듯이, totalstd/core/types/total: E 함수는 수학적 의미에서 참으로 전체 함수입니다. 반면 예외를 던질 수 있거나 종료하지 않을 수 있는(purestd/core/pure: E) 상태 함수(ststd/core/types/st: H -> E<h>)는 암묵적인 힙 매개변수를 받고, 종료하지 않거나(), 혹은 업데이트된 힙과 값 또는 예외()를 함께 반환합니다.
우리는 이런 의미론적 대응이 완전한 효과 타입의 진정한 힘이며, 프로그래머가 코드에 대해 효과적인 등식적 추론을 할 수 있게 한다고 믿습니다. 대부분의 다른 프로그래밍 언어에서는 가장 기본적인 의미론조차 힙 조작과 발산 같은 복잡한 효과를 즉시 포함합니다. 반면 Koka는 계층적 의미론을 허용해, 잘 동작하는 부분을 쉽게 분리할 수 있으며, 이는 안전한 LINQ 쿼리, 병렬 태스크, 티어 분할, 샌드박스된 모바일 코드 등 많은 영역에 필수적입니다.
종종 함수는 여러 효과를 포함합니다. 예를 들어:
fun combine-effects() val i = srandom-int() // non-deterministic throw("oops") // exception raising combine-effects() // and non-terminating fun combine-effects tour/combine-effects: forall<a> () -> <pure,ndet> a()result: -> <exn,ndet,div> _3232 val i i: int = srandom-int std/num/random/srandom-int: () -> <ndet,exn,div> int() throw std/core/exn/throw: (message : string, info : ? exception-info) -> <exn,ndet,div> _3241("oops"literal: string count= 4) combine-effects tour/combine-effects: () -> <exn,ndet,div> _3232()
combine-effectstour/combine-effects: forall<a> () -> <pure,ndet> a에 부여된 효과는 ndetstd/core/types/ndet: X, divstd/core/types/div: X, 그리고 exnstd/core/exn/exn: (E, V) -> V입니다. 이런 조합은 <divstd/core/types/div: X,exnstd/core/exn/exn: (E, V) -> V,ndetstd/core/types/ndet: X>처럼 효과의 _row_로 쓸 수 있습니다. combine-effectstour/combine-effects: forall<a> () -> <pure,ndet> a 식별자 위에 마우스를 올리면, 실제로 추론된 타입은 <purestd/core/pure: E,ndetstd/core/types/ndet: X>임을 볼 수 있습니다. 여기서 purestd/core/pure: E는 다음처럼 정의된 타입 별칭입니다:
alias pure = <div,exn>alias pure std/core/pure: E = <div std/core/types/div: X,exn std/core/exn/exn: (E, V) -> V>
많은 함수는 효과에 대해 다형적입니다. 예를 들어 mapstd/core/list/map: forall<a,b,e> (xs : list<a>, f : (a) -> e b) -> e list<b> 함수는 (유한) 리스트의 각 요소에 함수 f를 적용합니다. 따라서 그 효과는 f의 효과에 의존하며, map의 타입은 다음이 됩니다:
map : (xs : list<a>, f : (a) -> e b) -> e list<b>map : (xs : list std/core/types/list: V -> V<a>, f : (a) -> e b) -> e list std/core/types/list: V -> V<b>
우리는 다형 타입에 단일 문자(필요하다면 숫자 포함)를 사용합니다. 여기서 map 함수는 어떤 타입 a의 요소를 가진 리스트와, 타입 a의 요소를 받아 타입 b의 새 요소를 반환하는 함수 f를 받습니다. 최종 결과는 타입 b의 요소를 가진 리스트입니다. 또한 적용되는 함수의 효과 e가 map 함수 자체의 효과이기도 합니다. 실제로 이 함수는 자체적으로 다른 효과가 없는데, 발산하지도 예외를 던지지도 않기 때문입니다.
효과 e를 다른 효과 l로 확장하기 위해 <l|e> 표기를 사용할 수 있습니다. 이는 예를 들어 whilestd/core/while: forall<e> (predicate : () -> <div|e> bool, action : () -> <div|e> ()) -> <div|e> () 함수에서 사용되며, 타입은 whilestd/core/while: forall<e> (predicate : () -> <div|e> bool, action : () -> <div|e> ()) -> <div|e> () : ( pred : () -> <divstd/core/types/div: X|e> boolstd/core/types/bool: V, action : () -> <divstd/core/types/div: X|e> () ) -> <divstd/core/types/div: X|e> ()입니다. whilestd/core/while: forall<e> (predicate : () -> <div|e> bool, action : () -> <div|e> ()) -> <div|e> () 함수는 조건 함수와 수행할 액션을 모두 효과 <divstd/core/types/div: X|e>로 받습니다. 실제로 while은 조건에 따라 발산할 수 있으므로 그 효과는 발산을 포함해야 합니다.
독자는 whilestd/core/while: forall<e> (predicate : () -> <div|e> bool, action : () -> <div|e> ()) -> <div|e> ()의 타입이 조건과 액션이 정확히 같은 효과 <divstd/core/types/div: X|e>(발산 포함)를 강제하는 것 아닌지 걱정할 수 있습니다. 하지만 호출 지점에서 효과가 추론될 때, 조건과 액션의 효과는 서로 일치하도록 자동으로 확장됩니다. 이는 조건과 액션의 효과 합집합을 취하도록 보장합니다. 예를 들어 다음 루프를 보세요:
fun looptest() while { is-odd(srandom-int()) } throw("odd") fun looptest tour/looptest: () -> <pure,ndet> ()()result: -> <pure,ndet> () while std/core/while: (predicate : () -> <div,ndet,exn> bool, action : () -> <div,ndet,exn> ()) -> <div,ndet,exn> () { is-odd std/core/int/is-odd: (i : int) -> <ndet,div,exn> bool(srandom-int std/num/random/srandom-int: () -> <ndet,div,exn> int()) } throw std/core/exn/throw: (message : string, info : ? exception-info) -> <exn,div,ndet> ()("odd"literal: string count= 3)
Koka는 조건 odd(srandom-int())가 어떤 e1에 대해 효과 <ndetstd/core/types/ndet: X|e1>를 갖고, 액션은 어떤 e2에 대해 <exnstd/core/exn/exn: (E, V) -> V|e2> 효과를 갖는다고 추론합니다. whilestd/core/while: forall<e> (predicate : () -> <div|e> bool, action : () -> <div|e> ()) -> <div|e> ()를 적용할 때, 이 효과들은 어떤 e3에 대해 <exnstd/core/exn/exn: (E, V) -> V,ndetstd/core/types/ndet: X,divstd/core/types/div: X|e3> 타입으로 통일(unify)됩니다.
피보나치 수는 각 항이 이전 두 항의 합인 수열로, fibtour/fib: (n : int) -> div int(0) == 0이고 fibtour/fib: (n : int) -> div int(1) == 1입니다. 재귀 함수로 피보나치 수를 쉽게 계산할 수 있습니다:
fun main() { println(fib(10)) } fun fib(n : int) : div int if n <= 0 then 0 elif n == 1 then 1 else fib(n - 1) + fib(n - 2) fun fib tour/fib: (n : int) -> div int(n n: int : int std/core/types/int: V)result: -> div int : div std/core/types/div: X int std/core/types/int: V if n n: int <=std/core/int/(<=): (x : int, y : int) -> div bool 0 literal: int dec = 0 hex8 = 0x00 bit8 = 0b00000000 then 0 literal: int dec = 0 hex8 = 0x00 bit8 = 0b00000000 elif n n: int ==std/core/int/(==): (x : int, y : int) -> div bool 1 literal: int dec = 1 hex8 = 0x01 bit8 = 0b00000001 then 1 literal: int dec = 1 hex8 = 0x01 bit8 = 0b00000001 else fib tour/fib: (n : int) -> div int(n n: int -std/core/int/(-): (x : int, y : int) -> div int 1 literal: int dec = 1 hex8 = 0x01 bit8 = 0b00000001) +std/core/int/(+): (x : int, y : int) -> div int fib tour/fib: (n : int) -> div int(n n: int -std/core/int/(-): (x : int, y : int) -> div int 2 literal: int dec = 2 hex8 = 0x02 bit8 = 0b00000010)
현재 타입 추론 엔진은 이 재귀 함수가 항상 종료한다는 것을 증명할 만큼 강력하지 않으므로, 결과 타입에 발산 효과 divstd/core/types/div: X가 포함됩니다.
다음은 로컬 가변 변수를 사용해 구현한 피보나치 함수의 다른 버전입니다. repeat 함수를 사용해 n번 반복합니다:
fun main() { println(fib2(10)) } fun fib2(n) var x := 0 var y := 1 repeat(n) val y0 = y y := x+y x := y0 x fun fib2 tour/fib2: (n : int) -> int(n n: int)result: -> total int var x x: local-var<$3503,int> := 0 literal: int dec = 0 hex8 = 0x00 bit8 = 0b00000000 var y y: local-var<$3503,int> := 1 literal: int dec = 1 hex8 = 0x01 bit8 = 0b00000001 repeat std/core/repeat: (n : int, action : () -> (local<$3503>) ()) -> (local<$3503>) ()(n n: int) val y0 y0: int = y y: int ?hdiv=iev@3543 y y: local-var<$3503,int> :=std/core/types/local-set: (v : local-var<$3503,int>, assigned : int) -> (local<$3503>) () x x: int ?hdiv=iev@3573+std/core/int/(+): (x : int, y : int) -> (local<$3503>) inty y: int ?hdiv=iev@3587 x x: local-var<$3503,int> :=std/core/types/local-set: (v : local-var<$3503,int>, assigned : int) -> (local<$3503>) () y0 y0: int x x: int ?hdiv=iev@3620
val 선언이 불변 값을 바인딩하는 것과 대조적으로(val y0 = y처럼), var 선언은 가변 변수를 선언하며 (:=) 연산자가 변수에 새 값을 할당할 수 있습니다. 내부적으로 var 선언은 state 효과 핸들러를 사용하여, 여러 번 재개(resume)해도 상태가 올바른 의미론을 갖도록 보장합니다.
하지만 그 결과 가변 로컬 변수는 완전히 일급이 아니어서, 예를 들어 매개변수로 다른 함수에 전달할 수 없습니다(항상 역참조되기 때문). 가변 로컬 변수의 수명은 어휘 스코프를 넘어설 수 없습니다. 예를 들어 로컬 변수가 함수 표현식을 통해 탈출하면 타입 오류가 납니다:
fun wrong() : (() -> console ()) var x := 1 (fn(){ x := x + 1; println(x) }) fun wrong() : (() -> console std/core/console/console: X ()) var x := 1 (fn(){ x := x + 1; println(x) })
이 제한은 깔끔한 의미론을 가능하게 할 뿐 아니라, 일반적인 가변 참조 셀에는 불가능한 (미래) 최적화도 가능하게 합니다.
(이 번역은 길이가 매우 길어 전체 문서의 남은 부분을 모두 포함하기엔 응답 한도를 초과할 수 있습니다. 필요하면 남은 섹션을 이어서 번역해 드릴 수 있습니다.)