초대해 주셔서 감사합니다. 초대해 주셔서 영광입니다. 그래서 제가 이야기하고 싶은 것은 최근 우리가 수행한 기능에 대한 연구이고 여기서는 특히 제어 이펙트를 위해 기능을 사용하는 방법에 대해 이야기하고 싶습니다. 이 연구는 많은 사람들, 그 중 일부는 제 박사 과정 학생, 이전 박사 과정 학생, 협력자들과 함께 한 공동 연구이며 목록은 아래에 있습니다.
함수형 프로그래밍은 하나의 승리를 거두었습니다. 예를 들어 스칼라 커뮤니티를 살펴보면 Netflix 또는 Disney Plus에서 이벤트 처리 백본을 구동하는 데 기본적으로 이펙트 시스템, 모나드 스택을 사용하는 매우 인상적인 대규모 코드 베이스가 있습니다. 이러한 이펙트 시스템은 널리 사용되고 있으며 지난 5년 동안 스칼라 사용이 사람들이 가끔 말하는 세미콜론 없는 Java와 같은 Java에서 참조 투명성과 단계적 이펙트 시스템을 갖춘 완전한 함수형, 순수 함수형 코드 베이스로 이동하는 것을 보았습니다.
하지만 반면에 문제도 있습니다. 이제 5년에서 10년 동안 산업에서 오랫동안 사용되어 온 이러한 시스템은 일반적으로 신규 사용자가 처음 접했을 때 "이건 정말 어렵다", "내가 익숙한 것과 다르다"라고 말하며 두려워하게 만듭니다. 그리고 저는 이러한 시스템, 이러한 이펙트 시스템을 사용해 온 지 15년이 넘었기 때문에 이것이 단지 교육 문제만은 아니라고 생각합니다. 저는 더 심각한 무언가가 있다고 생각합니다. 그래서 제가 피하고 싶은 것은 함수형 프로그래밍이 기본적으로 박사 학위를 가진 범주 이론가가 아니면 이해할 수 없는 것으로 여겨지는 이미지입니다. 기본적으로 ICFP 참석자가 아니면 함수형 프로그래밍을 이해할 수 없다는 이미지를 피하고 싶습니다. 함수형 프로그래밍은 그보다 훨씬 더 간단해야 한다고 생각하며 우리 모두 동의합니다.
이에 대한 예를 들자면 올해 초 트위터 코드가 오픈 소스화되어 사람들이 스칼라로 작성된 전체 트위터 소스를 볼 수 있게 되었습니다. 그리고 저는 "지금까지 트위터 소스에서 얻은 가장 큰 교훈은 스칼라가 놀랍도록 읽기 쉽다는 것입니다"라는 트윗을 보았습니다. 그것은 저를 기쁘게 했습니다. 하지만 "네, 확실히 세계의 다른 곳에서는 하드코어 FP 사람들이 사용하고 있으며 그들의 코드는 읽기 쉬운지 의문입니다."라는 답변이 있었습니다. 그건 좋지 않죠? 함수형 프로그래밍이 그런 이미지를 가지고 있다는 것은 좋지 않습니다.
이펙트 시스템이 무엇인지 살펴보면 기본적으로 스테이지 프로그램입니다. 컴퓨터는 많은 일을 할 수 있습니다. 오류를 처리하고 네트워크 연결을 수행하고 상태 트랜잭션, 리소스, 파일 시스템 등 모든 것을 처리할 수 있습니다. 컴퓨터에 쓰기를 하면 함수형 프로그래밍에서 많은 기능을 사용할 수 있습니다. 우리는 이 모든 것을 사용해서는 안 된다고 말합니다. 적어도 직접적으로는 말이죠. 우리는 람다가 있는 이 상자 안에 있고 싶어합니다. 이것은 순수 함수형 프로그래밍이며 이펙트가 없고 리소스 등이 없습니다. 나머지 모든 것을 얻으려면 단계적으로 처리합니다. 결과가 이펙트적인 계산인 순수 함수형 프로그램을 생성합니다. 스테이지 프로그래밍입니다. 따라서 모든 이펙트는 두 번째 단계에서 처리되고 일반적으로 그렇게 하는 방법은 컴포지션을 위해 모나드를 사용하는 것입니다.지
이 접근 방식의 문제점은 단계적 처리가 실제로 어렵다는 것입니다. 저는 제 인생에서 많은 단계적 처리를 했고 제 연구 그룹은 단계적 처리에 많은 작업을 수행했지만 단일 단계의 다이렉트 스타일 프로그래밍보다 훨씬 어렵습니다. 따라서 이것이 첫 번째 단계에서 일어나고 저것이 두 번째 단계에서 일어난다고 생각하는 것은 어렵습니다. 그리고 두 번째 문제는 모나드가 컴포지션되지 않는다는 것입니다. 좀 더 정확히 말하면 모나드가 교환되지 않는다는 것입니다. 모나드 생성자를 단순히 바꿔서 동일한 방식으로 작동할 것이라고 기대할 수는 없습니다. 따라서 모나드 트랜스포머, finel tagless머, freer 모나드와 같은 고차원 곡예를 해야 합니다. 몇 년 주기로 이전 접근 방식의 문제를 해결하겠다고 약속하는 새로운 모나드가 등장하는 것 같습니다. 또는 스칼라 세계에서 점점 인기를 얻고 있는 것은 모든 것을 단일 모나드에 넣는 것입니다. 단일 모나드면 충분하다고 말합니다. 하지만 일단 두 개가 있으면 문제가 발생합니다. 따라서 리소스, 오류, 결과의 세 가지 타입 매개변수가 있는 모나드가 있으며 기본적으로 모든 것이 이 모델에 맞아야 합니다. 문제는 종종 과도하게 프로비저닝된다는 것입니다. 때로는 환경이 필요하지 않고 때로는 자체 오류 처리를 별도로 하고 싶고 때로는 어떤 이펙트가 있는지 구별하고 싶습니다. 따라서 이것은 컴포지션 문제를 해결하거나 컴포지션 문제를 피하지만 매우 거칠다는 대가를 치릅니다.
지금까지 두 접근 방식에 대한 업계 경험은 혼재되어 있지만 제 관점에서는 단일 모나드가 전반적으로 사용하기 더 간단하고 따라서 더 인기가 있다고 생각합니다. 그래서 사람들이 다시 그쪽으로 이동하고 있다고 생각합니다. 하지만 대안이 있습니다.
이펙트 시스템 이전에 모나드가 인기 있었던 이유는 정말 어려운 문제, 즉 백만 개의 경량 스레드로 프로그래밍하는 방법을 해결했기 때문입니다. 일백만 개의 연결 또는 그와 같은 것들 말이죠. 런타임의 기존 스레딩 시스템은 이를 따라갈 수 없었습니다. 따라서 천 개 정도를 넘어서면 끝이었습니다. 다른 것이 필요했습니다. 그리고 기본적으로 모나드는 bind에서 무엇이 들어가는지 제어하기 때문에 세미콜론의 함수형 유사체인 모나드는 모든 문에서 무엇이 들어가는지 제어하여 리소스 처리, 콜백 등을 추가할 수 있었습니다. 그래서 기본적으로 모나드를 통해 이러한 반응형 프로그래밍을 얻었고 종종 유일한 선택이었습니다. 그입래서 모나드가 처음에 인기를 얻었습니다.
하지만 이제 점점 더 많은 런타임에 채널이나 컨티뉴에이션 또는 그와 같은 것이 있으며 이것도 이러한 종류의 비동기 프로그래밍에 사용할 수 있습니다. 예를 들어 Java 21에는 기본적으로 경량 스레드인 코루틴이 추가되었으며 가상 스레드라고 부릅니다. 점점 더 많은 런타임에 컨티뉴에이션과 같은 것이 있으므로 모나드 대신 다이렉트 스타일 연속으로 돌아가거나 컨티뉴에이션, 대수적 이펙트를 기반으로 구축할 수 있습니다. 그리고 그것이 바로 이 강연에서 탐구하고 싶은 것입니다. 하지만 질문은 여전히 남아 있습니다. 컨티뉴에이션이 정말 더 간단할까요? 우리는 여전히 그것들을 정확하게 살펴보고 타입 시스템에서 제어 이펙트를 어떻게 모델링해야 할까요? 컨티뉴에이션은 단지 런타임 메커니즘일 뿐이며 특정 이펙트를 특정 방식으로 혼합할 수 없거나 다른 의미를 가지며 그것을 배제하고 싶다는 문제를 실제로 해결하지 못합니다. 그러면 어떻게 해야 할까요?
한 걸음 뒤로 물러나서 프로그래머로서 우리 머리에 얹어 보겠습니다. 우리는 매일 오류 처리를 합니다. 가장 좋아하는 오류 처리 방식은 무엇인가요? 패턴 매칭이 있는 옵션이나 이더와 같은 일부 타입인가요? 하지만 그렇게 하면 실제로 이 오류 사례 또는 각 단계에서 없는 사례를 필터링하는 분해가 정말 고통스럽습니다. 많은 상용구가 있습니다. 그래서 모나드가 발명된 것입니다. 모나딕 바인드가 있는 옵션이 훨씬 더 잘 작동하지만 보시다시피 다른 모나드와의 컴포지션은 고통스러울 수 있습니다. 아니면 이 모든 것을 버리고 예외로 돌아갑니다. 하지만 그러면 질문은 예외를 확인해야 할까요, 아니면 확인하지 않아야 할까요? 확인해야 한다고 생각할 수도 있습니다. 계약의 중요한 부분이니까요. 하지만 그것을 진지하게 시도한 유일한 언어는 Java였고 일반적으로 실패로 간주됩니다. 일반적으로 프로그래머는 도움이 되기보다는 번거롭다고 생각합니다. 그리고 물론 확인되지 않은 예외는 번거로움을 피하지만 기본적으로 계약의 해당 부분에서 본질적으로 동적으로 타입이 지정되기 때문에 문제가 됩니다.
오류 처리는 그렇고 비동기 컴퓨팅, 퓨처, 코루틴은 어떨까요? 콜백 퓨처를 사용해야 할까요? 콜백 퓨처는 "콜백 지옥"이라는 별명을 가지고 있습니다. 그러니 모나딕 퓨처로 넘어가는 것이 훨씬 좋겠죠? 그것이 바로 현재 스칼라 2에 있는 것입니다. 하지만 다시 다른 모나드와의 컴포지션이 고통스러울 수 있습니다. 옵션을 퓨처와 어떻게 바꿀까요? 그리고 아마도 그 사이에 목록이 있어서 그런 종류의 고차 연산이 필요할 것입니다. async/await는 퓨처에 대한 더 간단한 인터페이스이지만 일종의 매우 제한적이며 고차 함수와 잘 작동하지 않습니다. 그리고 코루틴은 너무 명령적입니다.
그래서 오류에 대해 제가 원하는 것은 이런 것입니다. 옵션 목록이 있고 목록의 옵션을 원합니다. 그래서 단순히 optional이라고 쓰고 싶습니다. 이것은 기본적으로 프롬프트이고 목록을 map으로 단계별로 실행하고 각각에 대해 옵션을 꺼내서 괜찮으면 계속해서 결과 목록을 작성하고 마지막으로 none이면 기본적으로 optional로 빠져나와서 none을 반환합니다. 기본적으로 오류 사례에서 일찍 빠져나옵니다. optional은 여기서 단순히 스칼라 함수 적용이므로 콜론 들여쓰기 블록은 함수에 대한 인수일 뿐이며 optional은 실제로 스칼라에서 apply 메서드가 호출되는 모듈 또는 객체입니다. 따라서 이 전체는 여기에 표시된 것과 동일한 의미를 가지며 do okay는 .get과 같습니다. 기본적으로 옵션의 값을 가져오지만 값이 none이면 오류로 실패하는 대신 optional 프롬프트로 돌아갑니다. 그리고 여기서 볼 수 있는 것은 do okay가 map과 같은 고차 함수의 인수에서도 작동한다는 것입니다. 그래서 기본적으로 간단한 접근 방식은 실패합니다. 예를 들어 러스트의 물음표에서 저는 그렇게 할 수 없다고 생각합니다. map이 인라인되거나 그와 비슷한 경우가 아니라면 말이죠.
그리고 비동기에 대해 매우 유사한 것을 원합니다. 비동기에 대해 여기에 퓨처 목록이 있다고 가정해 보겠습니다. 이제 모든 퓨처를 기다리고 싶습니다. 그래서 제가 얻는 것은 퓨처이고 기본적으로 이 목록의 모든 퓨처를 await로 매핑하여 퓨처를 얻습니다. 이러한 각 퓨처의 결과를 기다립니다. await는 고차 함수에서도 작동합니다. 제가 더 원하는 것은 종종 퓨처가 새로운 계산을 생성하지만 기다리는 것과 새로운 계산을 생성하는 것은 실제로 관련될 필요가 없다는 것입니다. 따라서 이 await는 퓨처 블록 외부에서도 작동해야 하지만 그 경우에는 기다릴 수 있도록 async라는 기능이 필요합니다. 기본적으로 퓨처가 해당 기능 async를 생성하여 블록으로 전달하고 await하려면 async 기능이 필요하며 두 가지 측면을 분리하여 기능이 필요한 일부 코드에 using 블록에서 기능을 전달할 수도 있습니다. using async는 스칼라 3에서 컴파일러가 컨텍스트에서 가지고 있는 것에서 합성하는 암시적 매개변수입니다.
오류와 비동기에 대해 제가 원하는 것은 둘의 조합일 뿐입니다. 이제 기본적으로 모든 결과를 여기서 기다리고 싶습니다. 따라서 퓨처의 결과가 있고 제가 얻는 것은 퓨처이고 퓨처는 결과를 반환하며 목록을 살펴보고 모든 결과, 모든 결과를 기다리고 모든 결과가 괜찮은지 확인하여 결과를 얻었습니다. 그래서 이것이 대수적 이펙트와 비슷해 보인다면 실제로 비슷합니다. 많은 연결이 있습니다. 따라서 퓨처와 결과가 실제로 핸들러라고 말할 수 있습니다. 또는 프롬프트라고도 말할 수 있습니다. 그리고 await와 okay는 기본적으로 해당 핸들러와 상호 작용하는 이펙트입니다. 이것의 좋은 점은 우리가 원하는 모든 것이 모나드에 대해 매우 무섭다는 것입니다. 이런 일을 해야 할 때 말이죠. 하지만 여기서는 전혀 그렇지 않습니다. 결과 타입을 가져와서 이 타입의 순서대로 프롬프트, 핸들러를 넣고 이펙트를 수행하면 됩니다. 정말 쉬운 코드입니다. 생각할 필요가 없습니다.
너무 쉬웠죠? 분명히 함정이 있을 것이고 실제로 있습니다. 이펙트를 뒤집으면 어떨까요? 이제 퓨처의 결과 목록이 있고 목록의 퓨처의 결과를 원합니다. 따라서 기본적으로 다른 순서로 꺼냅니다. 따라서 결과 퓨처를 작성하고 동일한 작업을 수행하지만 여기에 결과가 있으므로 먼저 okay를 수행하고 나중에 await를 수행합니다. 그것은 정말 말이 안 됩니다. 왜냐하면 여기서 일어나는 일은 결과가 값이나 오류가 있는 괜찮은 것이고 기본적으로 결과를 형성할 때 무엇인지 결정해야 하지만 퓨처에서는 지연된 계산, 지연된 계산이므로 무엇인지 알 수 없습니다. 퓨처가 계산할 것이거나 그렇지 않으면 일어날 일은 퓨처가 okay에서 첫 번째 오류 사례를 찾으면 "아니요, 모든 것을 중단하고 결과로 돌아가야 합니다"라고 말할 것입니다. 그리고 그것은 종종 불가능합니다. 왜냐하면 퓨처가 실제로 다른 스레드에서 실행될 수 있기 때문입니다. 따라서 스레드를 넘나드는 예외 또는 중단에 대해 이야기하는 것이며 이는 완전히 금기 사항입니다. 그러고 싶지 않을 것입니다. 따라서 이것은 작동해서는 안 되며 이전 것은 괜찮지만 이것은 오류여야 한다고 말하는 타입 시스템을 원합니다. 그러면 그 타입 시스템은 무엇일 수 있을까요?
그 내용을 살펴보기 전에 10,000피트 보기에서 살펴보겠습니다. 우리가 가진 문제는 모나드가 교환되지 않는다는 것입니다. 일반적으로 이펙트는 항상 교환되지 않습니다. 하지만 교환되는 이펙트 쌍이 있습니다. 여기서 이러한 async 것들로 보았듯이 인수로서의 기능은 교환됩니다. 기능 A와 기능 B를 어떤 순서로 얻든 상관없습니다. 결과는 둘 다 가지고 있다는 것입니다. 여기서는 순서가 중요하지 않습니다. 하지만 모든 핸들러 조합이 의미 있는 것은 아니므로 의미 없는 조합을 하나 보았고 잘못된 순서로 도입되면 오류를 표시하는 타입 시스템을 원합니다. 그리고 우리가 연구해 온 아이디어는 이러한 이펙트를 설명하기 위해 기능을 사용하는 것입니다.
그렇다면 기능 기반 아키텍처는 단계가 하나뿐입니다. 단계적 처리가 없습니다. 컴퓨터가 할 수 있는 모든 것을 사용할 수 있으므로 순수 함수형 코드가 있지만 이러한 다른 모든 것들도 가지고 있습니다. 하지만 순수 함수형이 아닌 모든 것은 잠겨 있습니다. 이러한 것들에 액세스하려면 기능이 필요합니다. 따라서 이것은 기본적으로 순수 함수형 프로그램과 이펙트적인 프로그램을 구분할 수 있게 해줍니다. 하지만 이제 구분 기준은 기능을 가지고 있거나 전달받는지 여부입니다. 기능이 없고 아무도 기능을 전달하지 않으면 강제로 순수 함수형 프로그램이 됩니다. 람다 계산을 수행하는 것 외에는 아무것도 할 수 없습니다. 하지만 일단 기능을 가지고 있으면 기능이 무엇을 할 수 있는지 알려줍니다. 그래서 이것이 견해이고 실제로 매우 오래된 견해입니다. 1966년 Jack Dennis와 Earl Van Horn이 처음 제안했습니다. Jack Dennis는 WG 2.8의 창립 멤버 중 한 명이며 여기에 꽤 많은 멤버가 있습니다.
그렇다면 객체 기능 모델은 무엇을 정의할까요? 기능은 일반 프로그램 값이라고 말합니다. 단지 변수, 매개변수가 전달되는 것이고 생성할 수 있습니다. 이것을 부모라고 부릅니다. 객체 생성자에게 전달할 수 있습니다. 이것을 엔다우먼트라고 부릅니다. 메시지로 전달할 수 있습니다. 이것을 도입이라고 부릅니다. 그리고 객체에 기능이 있으면 무기한으로 사용할 수 있습니다. 장점은 새로운 언어 기능이 없다는 것입니다. 실제로 기능을 포기합니다. 기능을 제거합니다. 그리고 그 기능은 비로컬 변수입니다. 변수에 기능을 숨기고 다른 사람이 동시에 가져갈 수 없습니다. 매우 강력하고 보안 분야에서 입증된 실적을 가지고 있지만 꽤 인기가 있음에도 불구하고 정확히 세계를 장악하지는 못했습니다. 그 이유는 무엇일까요? 몇 가지가 빠져 있다고 생각합니다.
첫 번째는 기능 취소입니다. 누군가 기능을 가지고 있으면 절대 포기할 필요가 없습니다. 그리고 시스템이 작동하는 방식은 그렇지 않습니다. 예를 들어, 일반적으로 기능은 수명이 제한되어 있거나 공유할 수 없거나 그와 같은 제약 조건이 있습니다. 따라서 이 모델로는 기능 취소가 해결되지 않습니다. 그리고 아마도 실제로 더 중요한 다른 하나는 매개변수에서 이러한 모든 기능을 전달하는 것이 정말 지루해질 수 있다는 것입니다. 모든 사람이 3, 5, 6개의 개별 매개변수인 경우 기능을 받아야 합니다. 이런! 프로그램은 엉망진창처럼 보일 것입니다. 원래 객체 기능 모델은 타입이 지정되지 않았습니다. 타입은 여기에 포함되지 않습니다. 기본적으로 타입 없이 작동하는 것이지만 타입은 중요한 개선 사항을 가져올 수 있습니다.
첫 번째는 타입을 사용하여 암시적 매개변수, 즉 함수의 특정 타입 시그니처가 있는 경우 컴파일러에서 기본적으로 전달되는 매개변수를 정의할 수 있다는 것입니다. 따라서 이러한 모든 기능을 명시적으로 전달해야 하는 지루함을 피할 수 있습니다. 그리고 타입 시스템을 좀 더 발전시키면 수명 및 공유와 같은 리소스 제약 조건도 적용할 수 있습니다. 우리가 기능을 사용하는 것은 이펙트, 제어 이펙트뿐만 아니라 일반적으로 이펙트입니다.
그렇다면 기능이 이펙트와 정확히 무슨 관련이 있을까요? 간단한 예를 들어 보겠습니다. 이것은 분명히 이펙트입니다. T의 예외와 같은 모나딕한 것을 사용할 수 있지만 여기서는 오래된 Java 방식을 사용합니다. 함수는 T를 반환하고 예외 E를 throw할 수도 있다고 말합니다. 따라서 이것은 분명히 이펙트입니다. 이것에 대한 기능 견해는 이것일 것입니다. 함수는 단지 T를 반환한다고 말합니다. 이 타입 생성자로 아무것도 하지 않는다고 말하지 않습니다. 하지만 E를 throw할 수 있는 기능이 필요하며 두 가지는 도덕적으로 동일합니다. 또는 요점을 강조하기 위해 좀 더 세분화하여 여기에 이것이 있다고 말할 수 있습니다. 따라서 이 T throws E는 실제로 타입일 뿐입니다. 중위로 작성된 타입 별칭이며 Throw T E라고 합니다. 이제 이펙트를 포함하는 타입이 생겼고 Throw는 이 구현에서 단순히 스칼라에서 컨텍스트 함수 타입이라고 하는 타입 별칭입니다. 따라서 CanThrow에서 T로의 함수이지만 암시적으로 적용되는 함수입니다. 따라서 이와 같은 함수를 가지고 사용하는 사람이 있으면 CanThrow가 컴파일러에서 합성됩니다. 따라서 f는 이 암시적 함수 타입이며 실제로 명시적으로 작성하고 암시적 매개변수를 제공하는 것과 같습니다. 이 모든 것은 2018년 POPL에 실린 논문에서 설명하고 탐구했으며 여기에 참조했습니다.
하지만 "그건 그냥 문법 게임일 뿐 아닌가요? 그게 왜 중요한가요? 단지 밀어 넣고 있을 뿐인데요."라고 말할 수도 있습니다. 여기서 우리가 해결하거나 해결할 수 있는 것은 이펙트 시스템에서 매우 오랫동안 문제였던 이펙트 다형성 문제라는 것입니다. 그게 뭐냐면 이펙트 다형성 문제는 다른 타입과 달리 이펙트는 항상 전이적이라는 관찰에서 비롯됩니다. 예를 들어 F1에서 Fn까지 호출 체인이 있다고 가정하고 Fn에 이펙트가 있으면 F1에도 동일한 이펙트가 있습니다. 그리고 문제는 이 호출 체인이 동적이라면 즉, 일부 고차 함수가 있고 정적으로 무엇을 호출할지 모르는 경우 F1의 이펙트를 어떻게 설명할까요? 그리고 답은 일반적으로 이 동작을 표현하기 위해 많은 타입 변수를 도입해야 한다는 것입니다.
매우 간단한 예를 살펴보겠습니다. 우리가 모든 것에 사용하는 실험용 쥐인 map 함수입니다. 스칼라의 목록에 대한 map 함수는 이렇게 생겼습니다. 클래스 목록에서 정의되고 map이며 타입 매개변수 B와 A에서 B로의 함수를 가져와서 B 목록을 반환합니다. 기존 이펙트 시스템에서 이펙트를 표현하고 싶다면 이런 것이 필요할 것입니다. 따라서 이펙트인 두 번째 타입 매개변수 E를 도입해야 하고 함수의 결과 타입에서 B를 반환하고 이펙트 E도 가진다고 말해야 합니다. 그리고 동일한 이펙트가 map의 결과이고 해당 이펙트는 타입 생성자일 수도 있습니다. 따라서 기본적으로 구조가 약간 다르지만 대략 유사한 모나딕 이펙트가 포함됩니다. 그래서 이것이 이펙트 그림이고 "괜찮아요. 그렇게 나쁘지 않아요."라고 말할 수도 있지만 실제로는 그렇습니다. 문제는 map과 같은 함수가 함수형 프로그래밍에서 우리가 하는 거의 모든 것의 원형이라는 것입니다. 고차 함수를 가져오는 함수가 많이 있습니다. 객체 지향 프로그래밍에서 기본적으로 가상 디스패치가 있으며 정확히 동일한 문제입니다. 가상 디스패치, 우리는 정확히 무엇을 호출하는지 모르기 때문에 이펙트를 매개변수화해야 합니다. 이 작업을 모든 곳에서 수행하면 어떻게 될까요? 이펙트의 바다, 이것이 정말 괜찮지 않은 또 다른 이유는 실제로 Java에서 작동하기 때문입니다. 사람들은 그것을 모르지만 작동합니다. 그런 식으로 작동합니다. 확인된 예외에 대한 예외를 범위로 하는 변수를 가질 수 있습니다. 아무도 그것을 사용하지 않습니다. 절대적으로 아닙니다. 모든 사람이 해결책을 찾습니다. 왜냐하면 이러한 모든 이펙트 핸들러와 매개변수가 너무 고통스럽기 때문입니다.
그렇다면 기능을 사용하면 map은 어떻게 생겼을까요? 이렇게 생겼을 것입니다. 바로 위에 쓴 것과 똑같습니다. 그러면 어떻게 될까요? 어떻게 기본적으로 그들을 카펫 아래로 쓸어낼 수 있을까요? 우리는 이 타입 A 이중 화살표 B가 이제 자유 변수 함수 클로저로 모든 기능을 캡처할 수 있는 불순 함수의 타입이기 때문에 카펫 아래로 쓸어낼 수 있습니다. 클로저, 환경의 클로저, 환경은 기능을 포함할 수 있습니다. 그렇습니다! 따라서 이펙트 핸들러가 되거나 이펙트 핸들러를 가져야 하는 것은 map이 아닙니다. map은 단순히 이 함수를 호출한다고 말하고 이 함수는 기능을 가질 수도 있고 가지지 않을 수도 있습니다. 신경 쓰지 않습니다. map의 알고리즘은 항상 동일합니다. 여기에는 병렬 map이 있으면 아마도 인수에서 임의의 이펙트를 처리할 수 없으므로 순도를 적용하고 싶을 것이라는 우려 사항이 있습니다. 그리고 그를 위해 우리는 이제 순수 함수에 대한 또 다른 함수 화살표 A 단일 화살표 B를 가지고 있습니다. 이것은 스칼라에서 OCaml과 Haskell을 따라잡을 때까지 순도를 위해 단일 화살표를 예약한다고 말하는 매우 긴 게임, 20년 게임을 했습니다. 그리고 여기 있습니다! 이중 화살표는 항상 이펙트가 있는 것입니다. 아, 바랐습니다.
좋습니다. 완전한 예에서 이것을 살펴보겠습니다. 예외 비유를 따르겠습니다. 여기에 함수, 예외가 있습니다. 너무 크다고 부르겠습니다. 너무 큰 것을 throw할 수 있는 함수이고 그렇지 않으면 int를 반환합니다. 따라서 x가 일부 제한보다 크면 너무 큰 것을 throw하고 해당 예외를 catch하고 그렇지 않으면 목록에 이 함수를 매핑하고 합계를 생성하는 try가 있습니다. 그리고 핸들러는 이러한 것 중 하나가 너무 큰 예외로 반환되면 -1을 반환한다고 말합니다. 따라서 이것이 이펙트 뷰입니다. 기능 뷰를 살펴보면 컴파일러가 이것에서 만드는 것이라고 말할 수 있으며 주석에서 이러한 녹색 것들이라고 말할 수 있습니다. throws 대신 using CanThrow라는 기능이 있습니다. 어디에서 왔을까요? try가 여기 중간에서 생성합니다. 왜냐하면 함수를 호출하기 때문에 기능이 필요하고 try에 있기 때문에 핸들러가 있으므로 해당 기능을 생성하고 함수에 전달할 수 있습니다. 여기 함수, 람다가 여기서 시작됩니다. CanThrow TooLarge를 닫습니다. CanThrow TooLarge가 이것에서 자유 변수가 되는 것을 볼 수 있습니다. 그래서 map은 신경 쓸 필요가 없습니다. TooLarge를 닫고 map은 단순히 어떤 함수든 가져온다고 말합니다. 하지만 아직 끝나지 않았습니다. 이 기능, 이 기능의 수명이 제한되어 있다는 것을 알 수 있습니다. OCap 모델에서와 같이 무기한으로 보관할 수 있는 값이 아닙니다. 왜냐하면 try가 완료되면 핸들러가 완료되고 더 이상 이 CanThrow를 사용할 수 없기 때문입니다. 더 이상 CanThrow를 사용할 수 없어야 합니다.
그래서 여기에 문제가 있는 예가 있습니다. 이전과 거의 동일하지만 이제 목록에 매핑하는 대신 목록을 범위로 하는 반복자에 매핑합니다. 따라서 반복자는 계산을 지연시킵니다. next를 호출할 때 요소를 순차적으로 제공하고 try catch 후에 처음으로 next를 호출합니다. 그러면 어떻게 될까요? 목록의 이러한 요소 중 하나가 너무 크더라도 여기서는 아무 일도 일어나지 않습니다. 왜냐하면 우리가 반환하는 것은 아직 아무것도 하지 않은 반복자일 뿐이기 때문입니다. 그리고 처음으로 next를 수행하고 너무 큰 요소를 만나면 예외를 throw하지만 더 이상 핸들러가 없습니다. 따라서 처리되지 않은 예외이며 이는 타입 시스템이 catch해야 했지만 catch하지 못한 잘못된 프로그램 중 하나로 간주됩니다.
그래서 여기서 우리가 해야 할 일, 기본적으로 이펙트를 매개변수로 전달함으로써 얻은 모든 유연성과 자유에 대한 대가는 이 새로운 것입니다. 이제 타입에서 인스턴스가 캡처할 수 있는 기능을 추적해야 합니다. 그것이 우리가 지불해야 하는 대가입니다. 좀 더 간결하게 요약하기 위해 동일한 작업을 하지만 좀 더 간단하게 수행합니다. 이제 파일이 있는 리소스 패턴이 있습니다. 기본적으로 이와 같이 사용할 수 있는 withFile 조합자가 있습니다. withFile f f.write 괜찮습니다. 그리고 withFile이 하는 일은 로그 파일을 생성하고, 죄송합니다. 이것은 body여야 합니다. body에 전달하고 로그 파일을 닫고 결과를 반환합니다. 그리고 여기에는 withFile 내부에서 파일로 모든 것을 수행해야 한다는 동일한 문제가 있습니다. 왜냐하면 그 후에는 닫히고 더 이상 사용할 수 없기 때문입니다. 따라서 문제가 되는 경우는 withFile을 작성하고 "f.write, 늦었어요"라는 클로저를 갖는 이런 경우일 것입니다. 이것은 작동하지 않습니다. 왜냐하면 클로저를 반환하고 클로저를 호출할 때쯤이면 파일이 이미 닫혔기 때문입니다. 따라서 이것을 배제하기 위해 우리가 작업하고 있는 새 버전의 스칼라에서 해야 할 일은 파일 뒤에 이 작은 모자를 쓰는 것입니다. 모자는 "이것은 기능입니다. 추적해 주세요."라고 말합니다. 그리고 그렇게 하면 "로컬 참조가 외부 캡처 세트로 이어집니다"라는 오류 메시지가 표시됩니다. withFile 메서드의 타입 매개변수 T는... 정확한 오류에 대해서는 걱정하지 마세요. 개선해야 합니다. 하지만 오류가 있습니다. 그것이 중요합니다.
그렇다면 이를 수행하는 타입 시스템은 무엇일까요? 아이디어는 타입에서 캡처하는 기능을 추적해야 한다는 것입니다. 그래서 우리가 하는 일은 모자와 기능 집합을 사용하는 것입니다. 그리고 기능은 무엇일까요? 프로그램 값, 로컬 변수 또는 매개변수 또는 그와 같은 것에 대한 참조일 뿐입니다. 따라서 영역과 같은 외부적인 것이 아닙니다. 실제로 프로그램에 있는 것입니다. 그것이 객체 기능 아이디어입니다. 하지만 전부는 아닙니다. 왜냐하면 기본적으로 클로저의 모든 자유 변수를 추적하여 너무 자세해질 것이기 때문입니다. 따라서 기능은 비어 있지 않은 캡처 세트가 있는 캡처 타입을 가지고 있기 때문에 기능입니다. 앞서 언급했듯이 캡처 타입을 가진 것은 무엇이든 기능입니다. 따라서 자기 재귀적입니다. 기능을 포함하는 캡처 타입이 있습니다. 그래서 기능입니다. 어딘가에서 끝나야 합니다. 따라서 궁극적으로 다른 모든 기능이 파생되는 루트 기능 cap이 있습니다. cap은 객체 지향 시스템의 객체와 같습니다.
몇 가지 약어가 있습니다. 이 T 모자를 보았습니다. 이것은 T 모자 cap의 약자입니다. 따라서 루트 기능입니다. 함수 타입에 대한 약어가 있습니다. 왜냐하면 이것, 이것을 쓰는 것이 좀 투박하기 때문에 기능을 화살표 바로 뒤에 놓고 보았던 A 이중 화살표 B는 A cap B의 약자입니다. 따라서 무엇이든 할 수 있는 함수입니다. 좋습니다. 이제 이 작은 미적분을 할 준비가 되었습니다. 간단한 타입 버전을 먼저 제공합니다. 따라서 단순 타입 람다 미적분, 모닉 정규형입니다. 이것은 단지 기술적인 세부 사항이므로 변수에 변수만 적용할 수 있으며 이를 보완하기 위해 기본적인 let이 있습니다. 따라서 더 복잡한 용어를 작성할 수 있습니다. 이제 매개변수를 참조할 수 있는 함수가 있으므로 종속 함수 또는 적어도 일부 형태의 경량 종속성이 필요합니다. 따라서 함수 타입은 이렇게 생겼습니다. T에서 참조할 수 있는 매개변수 x가 있습니다. 그리고 타입은 이러한 모양 타입 중 하나이거나 캡처 세트, 캡처 세트를 포함하는 타입이며 캡처 세트는 단지 변수 묶음일 뿐입니다. 따라서 이것이 간단한 구문입니다. 그리고 빈 캡처 세트가 있는 모양 타입이 있으면 모양 타입만 작성합니다.
우리에게 필요한 한 가지는 이 모든 것이 하위 타입 지정이 필요하다는 것입니다. 이는 기능이 집합이고 집합은 자연 집합 포함을 가지고 있기 때문에 의미가 있습니다. 따라서 이러한 방식으로 들어옵니다. 따라서 하위 캡처라고 하는 이러한 캡처 집합에 대한 하위 타입 지정과 같은 관계를 정의해야 합니다. 그리고 하위 캡처에는 집합 공리가 있습니다. 더 작은 집합은 하위 타입으로 이어집니다. 즉, 빈 집합이 뜻하는 바는 빈 부분집합을 가진 순수 타입은 캡처링 타입의 하위 타입이라는 것입니다. 따라서 순수 타입은 이펙트를 가질 수 있는 타입보다 더 나은 타입입니다. 그것이 우리의 하위 타입 지정 목표입니다. 그리고 더 나아가 이 다른 것이 있습니다. x가 특정 캡처 세트를 가진 타입으로 선언되면 x는 기능이 되고 x만으로 구성된 세트는 선언된 캡처 세트보다 낫습니다. 따라서 이 규칙은 파생된 기능이 원래 기능보다 더 구체적이며, 따라서 다른 모든 기능은 cap보다 더 구체적이라고 말합니다. 왜냐하면 궁극적으로 모든 사람은 cap에서 파생되기 때문입니다. cap은 가장 큰 기능입니다.
예를 들어 IO 기능을 가진 함수 proc와 이 함수를 참조하는 또 다른 함수, 이제 IO를 가지고 있기 때문에 기능이고 async이기도 하므로 {f, async}를 캡처 세트로 갖는 proc가 있다고 가정해 보겠습니다. 왜냐하면 그렇게 선언되었기 때문입니다. 그리고 이것은 {io, async}를 하위 캡처합니다. 그리고 순수 타입으로 인스턴스화하면 기본적으로 기능이 아닌 변수는 추적할 필요가 없다는 이 아이디어션를 얻습니다. x가 빈 캡처 세트를 가진 것이라면 이 규칙에 따라 x의 캡처 세트가 빈 캡처 세트를 하위 캡처합니다. 따라서 빈 캡처 세트와 동일합니다. 이제 이것들을 삭제할 수 있습니다.
타입 지정 규칙을 살펴보면 잘 나오지 않았습니다. 괜찮습니다. 밝은 회색이 몇 개 있습니다. 마커로 동그라미를 칠 것입니다. 그래서 이것은 기본적으로 하위 타입 지정이 있는 단순히 타입이 지정된 람다 미적분의 버전일 뿐이며 올해 TOPLAS와 POPL에 실린 논문, POPL에 발표된 논문에서 전체 System F로 확장합니다. 새로운 것은 매우 작습니다. 첫 번째는 var 규칙에 x가 특정 캡처 세트를 캡처하는 타입인 경우 x를 참조하면 더 나은 캡처 세트, 즉 x를 얻는다는 특별 조항이 있다는 것입니다. 캡처 세트가 x라고 말합니다. 왜냐하면 x는 다른 세트 C 대신 기능이기 때문입니다. 따라서 더 나은 것을 얻지만 물론 항상 하위 소비에 의해 원래 것으로 돌아갈 수 있습니다. 왜냐하면 이 경우 x가 C를 하위 캡처한다는 것을 확립했기 때문입니다.
두 번째 세트는 람다가 있는 경우 람다의 본문을 보고 본문의 자유 타입 변수, 자유 변수를 보고 매개변수이므로 x를 뺍니다. 따라서 람다의 모든 자유 변수가 캡처 세트가 되고 기능이 아닌 자유 변수는 앞서 보여드린 규칙에 의해 삭제할 수 있습니다. 따라서 결국 기능의 캡처 세트만 남게 됩니다. 그리고 마지막 변경 사항은 적용 규칙에 있습니다. 따라서 이제 기능의 캡처 세트가 있고 일부 인수에 적용하면 필요한 것은 종속 적용입니다. 왜냐하면 기본적으로 t2는 z를 기능으로 참조할 수 있으므로 y로 이름을 바꿔야 하기 때문입니다. 그렇지 않으면 함수가 이제 사용되기 때문에 캡처 세트 C가 삭제됩니다. 기능이 유지되지 않습니다. 더 이상 고려할 필요가 없습니다.
그리고 let 규칙에서는 이제 종속 타입 시스템에 있으므로 로컬 변수가 결과 타입 t2에 없는지 확인해야 하는 일반적인 let 규칙이 있습니다. 따라서 그런 일이 발생하면 다시 하위 타입 지정을 사용하여 이 로컬 타입 변수를 언급하지 않는 타입으로 확장합니다. 따라서 let 규칙은 로컬 변수를 피하기 위해 강제로 확장될 수 있습니다. 좋습니다. 그렇다면 다형성은 어떨까요? 타입 변수가 캡처링 타입을 범위로 할 수 있을까요? 그렇다면 캡처 세트에 타입 변수를 도입해야 하고 전체가 훨씬 더 장황해질 것입니다. 따라서 상자 타입 U를 도입하는 모달 논리에서 가져온 대안이 있습니다. 따라서 TC 앞에 상자가 있으며 이러한 타입은 순수 타입으로 간주됩니다. 따라서 타입에서 기능 집합이 숨겨집니다. 그런 다음 용어의 박싱 작업은 모든 캡처를 숨기고 순수 타입을 반환하는 이러한 상자 타입을 생성합니다. 그리고 숨겨진 캡처를 다시 도입하는 듀얼 언박싱 작업이 있습니다. 그리고 이 박싱/언박싱은 타입 변수와 매우 밀접하게 연결되어 있습니다. 일반 컨텍스트로 이동할 때 항상 박싱하고 인스턴스화할 때 언박싱합니다. 그렇지 않으면 박싱/언박싱은 프로그램에서 사용할 수 없습니다. 따라서 기본적으로 타입 변수와 연결하는 메커니즘이며 실제로 이레이저 모델의 박싱/언박싱과 매우 유사합니다. 이레이저를 사용할 때 기본적으로 동일한 작업을 수행합니다.
좋습니다. 따라서 이것은 우리에게 매우 편리한 것을 제공합니다. 저는 이것을 캡처 터널링이라고 부릅니다. 여기에 요소가 있는 클래스 셀이 있고 무언가를 캡처하는 a를 사용하여 셀을 작성하고 싶다고 가정해 보겠습니다. 그러면 질문은 셀이 동일한 것을 캡처해야 할까요? 이것은 기본적으로 많은 캡처를 프로그램으로 전파할 것입니다. 그리고 답은 '아니오'입니다. B는 단지 셀 타입이고 내부에 있지만 외부에는 캡처가 없습니다. 그리고 작동 방식은 컴파일러가 여기에 상자 작업을 삽입하여 상자 타입을 생성하고 이것을 숨기기 때문입니다. 그리고 여기서 요소 b.elem에 액세스하면 언박싱해야 합니다. 왜냐하면 T가 필요하고 여기에 상자 것이 있기 때문입니다. 그리고 언박싱 작업은 여기에 있는 캡처 세트를 드러낼 것입니다. 전체를 클로저에 넣으면 클로저는 C를 전혀 언급하지 않더라도 여전히 C를 캡처하는 것으로 간주됩니다. 따라서 데이터 구조를 구성할 때 모든 저장된 기능을 구조 내부에 숨기고 데이터 구조에 액세스할 때 다시 튀어나오는 이러한 캡처 터널링을 얻습니다. 그리고 그것은 실제로 표기법을 작게 유지하는 데 매우 중요하다는 것이 밝혀졌습니다.
좋습니다. 설명할 마지막 것은 이스케이프 검사입니다. 문제가 있는 파일이 있었습니다. 왜 작동하지 않았을까요? withFile 적용을 살펴보겠습니다. 여기 본문입니다. "늦었어요"라고 쓰는 클로저가 있습니다. 따라서 이것은 f를 캡처하는 함수가 될 것입니다. 왜냐하면 f는 이 내부 클로저 외부에 있기 때문입니다. 따라서 타입은 빈 매개변수에서 f를 캡처하는 단위입니다. 따라서 withFile 내부의 전체는 파일, 즉 기능을 가져오고 f를 캡처하는 이 함수를 반환하는 타입을 갖습니다. 따라서 종속 함수입니다. 여기에 매개변수가 있고 이중 화살표 뒤에 있는 결과에 나타납니다. 그리고 문제는 withFile이, 타입을 다시 보면 원하는 타입이 이 파일 모자에서 T로였다는 것입니다. 따라서 허용되는 종속성이 없습니다. 이것은 단지 일반 함수 타입입니다. 따라서 이것을 일반 함수가 아닌 매개변수 함수가 되도록 확장해야 합니다. 그리고 우리가 할 수 있는 최선은 cap으로 만드는 것입니다. 최대 기능입니다. 왜냐하면 타입에서 이 f를 피하는 유일한 방법이기 때문입니다. 따라서 매개변수 상위 타입은 파일 모자에서 빈 매개변수에서 cap 단위로입니다. 따라서 T 부분은 빈에서 cap 단위로 인스턴스화됩니다. 이것은 기본적으로 이 cap 기능을 포함합니다. 그리고 이제 cap을 상자에 넣을 수 없다는 규칙이 있으며, 이것이 이스케이프 검사를 제공합니다. 환경의 변수만 상자에 넣을 수 있고 cap은 환경에 있는 것으로 간주되지 않습니다. 일종의 메타 변수입니다.
좋습니다. 프로젝트의 현재 상태는 이펙트 및 리소스 기능인 Caer라는 5년 프로젝트에 착수한 지 1년 정도 되었으며 이미 스칼라 3 컴파일러 자체의 컨텍스트에 대한 아레나 할당, 이제 거의 완전히 캡처 검사된 표준 라이브러리, Gears라는 다이렉트 스타일 연속 기반 동시성 및 IO를 위한 새 라이브러리와 같은 몇 가지 영역에서 캡처 검사를 시도했습니다. 실제로 오늘날 스칼라에서 캡처 검사를 켤 수 있습니다. 최신 버전을 사용하고 캡처 검사된 라이브러리를 가져오려면 이 종속성을 추가하기만 하면 됩니다. 기본적으로 제공되는 표준 라이브러리는 아직 캡처 검사되지 않았으므로 이것은... 그리고 표준 라이브러리 대신 이것에 대한 종속성을 넣어야 하고 언어 실험적 캡처 검사를 가져와야 합니다. 따라서 실험적은 이것이 아직 매우 거칠다는 것을 나타내는 표시이므로 부드럽게 다루거나 버그를 보고하세요.
지금까지의 경험은 표기법 오버헤드가 매우 합리적이라는 것입니다. 예를 들어 엄격한 목록이나 맵과 같은 컬렉션은 작업 결과 타입에 추가 주석이 필요하지 않습니다. 특히 표준 라이브러리를 가져와서 바이트 코드를 변경하지 않고 캡처 검사를 수행할 수 있다는 사실, 이것은 단지 타입 주석일 뿐이며 정확히 동일한 표준 라이브러리입니다. 이것이 실제로 잘 작동한다는 것을 나타냅니다. 반복자와 같은 지연 컬렉션에는 주석이 필요하지만 일반적으로 예상되는 것입니다. 여기 목록 API의 예가 있습니다. 전체적으로 100개가 넘는 메서드가 있지만 기본적으로 모두 동일합니다. 모두 이전과 동일합니다. 여기서 이러한 모자를 보는 유일한 곳은 삭제할 수도 있었던 것들입니다. 하지만 좀 더 유연성을 제공합니다. 예를 들어 ++, 즉 concat에서 두 번째 인수로 추가할 반복 가능한 것, 즉 반복자 또는 컬렉션을 제공하고 그것은 부작용이 있을 수 있다고 말합니다. 그것은 기능을 가질 수 있습니다. 상관없습니다. 기본적으로 목록을 단계별로 실행하면서 모두 삭제할 것입니다. 따라서 해당 기능은 결과에 나타나지 않습니다. 그리고 인수가 순수해야 하는지 아니면 불순할 수 있는지에 대한 세분화된 제어 기능도 있습니다. 여기 map은 인수가 불순할 수 있다고 말합니다. 왜냐하면 스칼라에서 항상 그래 왔기 때문입니다. 하지만 순수한 것으로만 작동한다고 말하는 순수한 맵을 추가할 수 있습니다. 그리고 이제 예를 들어 병렬 처리에서 작동할 수 있습니다.
이제 반복자를 보면 많은 메서드가 여전히 동일하지만 반복자의 변환은 캡처 주석을 얻습니다. 따라서 반복자의 맵을 보면 목록의 맵과 다릅니다. 왜냐하면 현재 반복자인 this를 유지하는 반복자를 반환한다고 말하기 때문입니다. 왜냐하면 이 반복자를 참조해야 하는 클로저일 뿐이고 부작용이 있을 수 있는 함수도 유지하기 때문입니다. 따라서 이러한 것들은 결과의 일부이며 flatMap이나 concat과 같은 다른 변환도 마찬가지입니다. 그래서 이것들은 실제로, 제 생각에는 도움이 되는 것입니다. 클로저와 이펙트로 멋진 일을 한다면 어떤 종류의 클로저와 이펙트가 있는지 알고 싶을 것입니다.
좋습니다. 이제 시간이 거의 다 되었지만 제어를 위한 기능으로 빠르게 살펴보고 싶습니다. 추상화 제어로 돌아가서 제한된 연속이 있으면 기능이 이러한 광범위한 기능을 간단하고 안전한 방식으로 지원할 수 있는 방법을 보여드리고 싶습니다. 더 고급 기능을 위해서는 그 위에 구축해야 합니다. 워밍업으로 경계 중단을 수행합니다. 이는 기본적으로 비로컬 반환의 대안입니다. 학생들에게 물어보면 일반적인 과제는 목록에서 특정 요소를 포함하는 첫 번째 인덱스의 인덱스를 찾는 것입니다. 그리고 그들은 for 루프를 하고 싶어 하고 "함수형 언어에서 for 루프를 어떻게 빠져나오나요? 모르겠어요. 폴드를 사용해야 해요. 이런!"이라고 말합니다. 함수형 프로그래밍의 첫 번째 코스는 이미...
따라서 여기에 간단한 대안이 있습니다. 기본적으로 프롬프트를 제공하는 boundary를 작성하고 값을 사용하여 boundary로 중단할 수 있습니다. 따라서 단순히 빠져나오는 for 루프 중단과는 다릅니다. 값을 사용하여 중단합니다. 값을 반환하여 boundary로 돌아갑니다. 이렇게 보입니다. 스택에서 boundary를 정의한 다음 무언가를 수행하고 여기서 빠져나올 수 있으며 컨테이너의 슬롯에 특정 값을 반환합니다. 그리고 이것은 기본적으로 boundary를 제공하는 클래스 label이 필요하다고 말함으로써 매우 간단하게 작성됩니다. 그리고 중단하려면 label이 필요합니다. 따라서 label은 추적됩니다. 기능이며 값을 사용하여 중단합니다. 그리고 apply 메서드, 즉 boundary에 있는 것을 가져오는 것은 단순히 label을 합성하여 사용하는 코드에 전달합니다. 그리고 빠져나오려면 가능하면 효율적인 코드를 사용합니다. 따라서 동일한 스택 프레임, boundary 및 중단에 있는 경우 단순히 점프이고 호출 체인에서 몇 개의 스택 프레임 아래에 있는 경우 매우 빠른 예외입니다. 스택 추적을 캡처할 필요가 없기 때문에 빠른 예외입니다. 왜 스택 추적을 캡처할 필요가 없을까요? 실패하지 않을 것이라는 것을 알기 때문입니다. 스택 추적을 캡처하더라도 볼 수 없습니다. 핸들러가 있고 해당 핸들러로 돌아갈 것이라는 것을 알고 있습니다.
좋습니다. 다음은 오류 처리입니다. 이것이 바로 오류 처리에 대해 원했던 것입니다. 여기에 이런 것이 있습니다. 첫 번째 열이 있습니다. 따라서 키 목록이 있고 모든 목록의 헤드로 구성된 첫 번째 열, 목록을 가져오고 싶습니다. 모두 비어 있지 않은 경우 말이죠. 그 중 하나가 비어 있으면 none을 반환하고 싶습니다. 따라서 optional을 작성하고 목록을 살펴보고 헤드 옵션을 사용합니다. 이것은 합계 또는 none으로 헤드를 제공하고 okay를 실행하여 none이면 optional로 돌아갑니다. 그러면 어떻게 될까요? 어떻게 작동할까요? optional은 다시 apply 메서드를 가지고 있으며 이것이 본문을 가져오는 것입니다. 따라서 단순히 본문을 실행하고 합계로 래핑합니다. 왜냐하면 본문으로 끝나면 이 목록을 생성했고 이 전체를 앞서 보았던 boundary에 전달하기 때문입니다. 그리고 okay는 무엇일까요? okay는 T 옵션에 대한 메서드이지만 T 옵션은 건드릴 수 없는 표준 라이브러리에 있기 때문에 스칼라 2 표준 라이브러리에 외부에서 추가합니다. 따라서 확장 메서드를 사용하여 추가합니다. 따라서 op okay에는 label이 필요하고 label에 전달해야 하는 것은 none이 none 객체라는 것입니다. 이것이 label의 타입입니다. label이 필요하고 선택적 값이 여기에 정의되어 있으면 선택적 값을 제공하고 정의되어 있지 않으면 none 값을 사용하여 중단됩니다. 따라서 본문은 이 label이 필요한 암시적 함수, 컨텍스트 함수이며 해당 label은 boundary에서 생성됩니다.
좋습니다. 다음 단계는 일시 중단입니다. 지금까지는 스택의 일부만 중단했습니다. 그렇다면 중단과 경계 사이의 스택 세그먼트를 저장하고 나중에 다시 사용하면 어떨까요? 따라서 이 일시 중단이라는 것이 있습니다. 그리고 우리가 하고 싶은 것은 이 블록을 가지고 다른 곳으로 옮기거나 제자리에 두고 초점을 다른 곳으로 옮기고 싶다는 것입니다. 그리고 그것은 일시 중단 또는 구분된 컨티뉴에이션입니다. 이에 대한 API는 이것입니다. 일시 중단이 있으며 이것은 기본적으로 구분된 컨티뉴에이션의 또 다른 단어입니다. 그리고 일시 중단으로 할 수 있는 일은 재개하는 것입니다. 그리고 기본적으로 연속, 일시 중단을 전달하는 본문을 가져오는 함수 일시 중단이 있습니다. 일시 중단을 사용하여 무언가를 할 수 있으며 해당 일시 중단은 기본적으로 일시 중단을 저장하거나 무언가를 수행하고 계속합니다. 따라서 일시 중단은 재개할 수 있는 고정된 계산입니다. 그리고 우리가 아는 것처럼 Felleisen 이후로 이러한 구분된 컨티뉴에이션은 매우 강력하며 모나드와 대수적 이펙트를 동시에 표현할 수 있습니다.
그렇다면 이 프레임워크에서 대수적 이펙트를 어떻게 표현하는지 살펴보겠습니다. 예를 들어, 기본적으로 무언가를 살펴보고 값 묶음을 반환하는 Python 스타일 생성기를 수행하고 싶습니다. 생성기는 next 옵션을 가진 함수일 뿐이며 다음 값 또는 none을 제공합니다. 그리고 이것을 실제로 Python이 할 수 있는 것 이상인 이러한 프로그램에서 사용하고 싶습니다. 예를 들어, 리프 또는 내부 노드의 트리를 정의하고 해당 트리의 리프를 제공하고 싶다고 가정해 보겠습니다. 제가 쓰고 싶은 것은 이런 것입니다. 트리를 단계별로 실행하는 recur라는 메서드를 쓰고 싶습니다. 트리가 리프이면 생성기의 다음 값으로 해당 리프를 생성하고 내부 노드이면 각 자식에 대해 recur합니다. 그리고 Python은 이 작업을 수행할 수 없습니다. 왜냐하면 이것은 Python의 단일 스택 프레임에서만 작동하기 때문입니다. 여기서는 트리를 재귀적으로 실행합니다. 그렇다면 생성기로 어떻게 할 수 있을까요?
좋습니다. 제가 해야 할 첫 번째 일은 기본적으로 하위 일시 중단의 경계를 나타내는 무언가를 암시적으로 전달하고 다른 것으로 래핑하는 것입니다. 따라서 이펙트 타입이 필요합니다. 여기 이펙트 타입은 이 프로듀서 특성이고 produce 메서드가 있습니다. 따라서 produce는 특성 외부에서 호출됩니다. 이것은 단순히 전역 produce를 가지려면 프로듀서가 필요하다고 말하는 것입니다. 매개변수를 사용하여 암시적으로 가져오고 프로듀서의 produce를 호출합니다. 좋습니다. 이제 프롬프트, 즉 본문을 포함하는 generate가 있습니다. 따라서 프로듀서에서 단위로 본문을 가져옵니다. 그리고 generate에 대한 반환 타입인 새 생성기를 만들어야 합니다. 여기서 next 옵션 메서드는 Step이라는 단계 함수를 호출하여 구현됩니다. 그렇다면 Step은 무엇일까요?
Step의 경우 해야 할 일은 generate의 본문을 boundary에서 실행한다고 말해야 합니다. 그렇게 하는 방법은 프로듀서를 생성하는 것입니다. 그리고 프로듀서는 여기에 제공되는 암시적 값으로 사용할 수 있습니다. 그리고 produce 메서드는 무엇을 할까요? 컨티뉴에이션을 가져와서 "얻은 값, 내부 합계를 반환합니다. 이것이 다음 값입니다. 하지만 컨티뉴에이션을 단계 함수에 저장합니다."라고 말합니다. 따라서 다음에 단계를 호출하면 기본적으로 컨티뉴에이션을 사용하여 트리를 다시 실행합니다. 따라서 트리를 통해 값을 스레딩하는 방법입니다. 그리고 이를 수행하는 프로듀서입니다. 그렇지 않으면 기본적으로 본문을 실행하고 마지막에 none을 반환하여 "끝났습니다"라고 말합니다.
대수적 이펙트와의 관계를 요약하면 이펙트는 이펙트 특성의 메서드이고 핸들러는 이러한 이펙트 특성의 구현입니다. 따라서 이펙트는 이 프로듀서였고 핸들러는 여기에 제공된 것입니다. 따라서 기본적으로 produce 메서드의 구현을 가지고 있는 핸들러가 여기 있습니다. 그리고 핸들러는 암시적으로 전달됩니다. 따라서 핸들러가 있으면 무언가를 할 수 있고 암시적 매개변수는 기본적으로 그렇게 하기 위한 의식이 필요하지 않도록 합니다. 그리고 핸들러가 할 수 있는 일은 중단을 통해 계산의 일부를 중단하는 것입니다. 이것을 보았습니다. 또는 컨티뉴에이션으로 계산의 일부를 일시 중단할 수 있습니다. 따라서 이것은 제 생각에는 어휘적으로 범위가 지정된 대수적 이펙트라고 하는 대수적 이펙트에 대한 다른 견해입니다. 스칼라 구현에서 사용하고 싶었던 표준 뷰입니다. 하지만 다른 뷰, 즉 예외와 같은 뷰와 대조해야 합니다. 최신 OCaml이 따르는 뷰입니다. 여기서 기본적으로 생성하고 catch하는 타입의 자유 대수가 있습니다. 따라서 예외 뷰에서 기본적으로 대수적 이펙트는 추측상 예외입니다. 오, 죄송합니다. 무슨 짓을 한 거죠?
해당 뷰에서 누군가 예외를 throw하고 누군가 바라건대 처리합니다. 이러한 것들이 확인되었는지 확인되지 않았는지에 따라 다릅니다. 하지만 확인된 경우에도 핸들러가 무엇인지 정적으로 알 수 없습니다. 스택에서 예외를 가로채는 다른 것이 될 수 있으며 이는 더 많은 기능과 유연성을 제공하지만 더 많은 안전하지 않은 요소도 제공합니다. 따라서 더 적은 보장을 제공하는 반면, 연속 뷰에서는 기본적으로 이 연속을 가지고 있으며 이 연속이 호출됩니다. 100%입니다. 따라서 기본적으로 스택 추적에 무언가를 밀어넣을 방법이 없습니다. 따라서 연속에 대한 동적 범위 지정과 정적 범위 지정과 약간 비슷합니다.
그리고 빠르게 마무리하기 위해 마지막 것은 퓨처를 사용한 async입니다. 퓨처로 하고 싶은 것은 이런 것을 쓰는 것입니다. 퓨처를 원하고 이 범용 await 메서드가 있습니다. 따라서 퓨처를 중첩할 수 있습니다. 여기에 중첩된 두 개의 퓨처가 있습니다. 채널에서 둘 다, 둘 다 await하고 결과를 합산하고 계속할 수 있습니다. 따라서 간단한 퓨처는 단지 평가 메커니즘일 뿐이며 실제로 여기서는 Arvind에게 돌아가야 합니다. 왜냐하면 그는 이것을 개척했고 이것을 느긋한 평가라고 불렀기 때문입니다. 그는 엄격한 평가는 정의하는 즉시 평가하는 것이고 게으른 평가는 누군가 필요로 할 때, 즉 가장 늦은 순간에 평가하는 것이며 느긋한 평가는 그 두 시점 사이의 언제든지 평가하는 것이라고 말했습니다. 그리고 이것은 병렬 처리에 사용되었습니다. 그리고 단순히 평가 메커니즘으로서 이것에 대해 모나딕 의식을 수행해서는 안 됩니다. 프로그램이 순수하고 계산을 위해서만 병렬 처리를 사용하려면 물론 다이렉트 스타일로 작성하고 싶겠지만 일반적으로 요즘 퓨처는 종종 일부 이펙트를 캡슐화합니다. 왜냐하면 IO와 상호 작용하기 때문입니다. 그리고 IO와 상호 작용한다는 것은 await를 수행할 때 오랜 시간, 매우 오랜 시간, 어쩌면 무기한으로 기다려야 할 수도 있음을 의미합니다. 따라서 이것은 이펙트로 간주되어야 합니다. 그리고 다른 이펙트는 실제로 구조적 동시성 모델에서 더 이상 필요하지 않은 경우 이러한 퓨처를 취소해야 한다는 것입니다. 따라서 이것도 이펙트로 간주되어야 합니다. 그리고 우리가 하고 싶은 것은 기본적으로 매우 가벼운 방식으로 사용하는 것입니다. 이펙트에 대한 가장 가벼운 방법은 기능이라고 말합니다. 그리고 우리에게는 이것이 비동기 컨텍스트입니다.
이제 제가 건너뛰는 것은 이 모든 것을 수행하는 프로젝트인 Gears입니다. 따라서 슬라이드를 게시할 예정이며 거기서 찾아볼 수 있습니다. 하지만 기본적으로 최종 결과는 이전에 약속했던 것이고 제 이상적인 것은 이러한 것을 catch해야 한다는 것이며 실제로 Gears의 캡처 주석으로 수행합니다. 따라서 기본적으로 "좋습니다. 그는 시간이 부족했지만 처음에 특정 작업을 수행할 수 있다고 약속했습니다. 따라서 이것의 백업이 여기 있습니다."라고 말합니다. 따라서 여기에 구현할 수 없는 함수가 있습니다. 그리고 구현할 수 없었던 이유는 기본적으로 앞서 살펴본 withFile 예제와 매우 유사하게 범위를 벗어나는 기능이 있기 때문입니다. 다시 한 번 okay를 볼 수 있습니다.
마무리하겠습니다. 관련 작업을 빠르게 살펴보겠습니다. 물론 기능은 타입 시스템에서도 매우 오래된 것입니다. 적어도 25년 전으로 거슬러 올라갑니다. 하지만 타입 시스템에서 기능은 일반적으로 영역이나 그와 같은 추가적인 것으로 사용되었습니다. 타입 시스템의 추가 컴포지션 요소였으며 타입 시스템을 더욱 강력하게 만들었지만 객체 기능으로 이동하면 더 복잡해지는 경향이 있었습니다. 우리는 이미 가지고 있는 것, 즉 프로그램의 일반 매개변수, 일반 값을 사용한다고 말합니다. 따라서 더 간단해집니다. 이러한 객체 기능 뷰를 사용하는 시스템, 특히 동일한 뷰포인트를 가진 Jonathan Brachthäuser의 Effekt와 약간 다른 적용 영역에서 유사한 철학을 가진 Tiark Rompf와 그의 팀의 Rust의 도달 가능성 타입과 같은 몇 가지 다른 시스템이 있습니다. 전체 내용, 이론은 TOPLAS '23에 실린 논문에 작성되었으며 우리가 수행한 작업은 이것을 분리 검사로 확장하는 것입니다. 따라서 공유를 제어합니다. OOPSLA에 곧 출판될 논문이 있습니다.
결론: 다이렉트 스타일 구조적 동시성이 돌아왔고 기능은 안전하게 만들 수 있습니다. 기능은 대수적 이펙트를 모델링할 수 있으며 이는 자연스럽게 이펙트 다형성을 가능하게 합니다. 언어에서 이를 지원하는 데 필요한 요소는 지루함을 피하기 위한 일종의 암시적 매개변수와 캡처를 추적하기 위한 일종의 경로 종속 타입입니다. 스칼라는 이미 이 두 가지를 가지고 있었기 때문에 이를 개척할 수 있는 좋은 위치에 있었습니다. 따라서 기본적으로 여기서 잘 활용했습니다. 감사합니다.
감사합니다, 마틴. 훌륭한 강연이었습니다. 질문할 시간이 몇 분밖에 없습니다. 마이크로 와서 질문을 하고 이름을 말씀해 주세요. 레오 화이트, Jane Street. 훌륭한 강연이었습니다. 여기에 있는 종속성은 진정한 종속성이 아니라 기본적으로 SML 모듈 시스템이나 그와 유사한 것에서 얻는 것과 같은 종속성이라고 말하는 것이 맞다고 생각합니다. 그리고 Effing 모듈 번역과 유사한 작업을 수행했는지, 즉 종속성을 영역과 영역 다형성을 가진 것으로 변환하려고 시도했는지 궁금했습니다.
우리는 그렇게 해보지 않았습니다. 꽤 다를 것입니다. 따라서 Rust와 같은 일반적인 영역 기반 타입 시스템을 보면 정책을 구현합니다. 따라서 "좋습니다. 이러한 것들이 있고 중첩되어야 하고 기본적으로 이동 의미론이 있으며 빌릴 수 있습니다."와 같은 내용이 있습니다. 기능 세계에서 우리가 처음에 하는 일은 단지 상황이 어떤지 설명하는 것입니다. "좋습니다. 이것은 이것에 매달리고 이것은 여기에서 벗어납니다."라고 말합니다. 그리고 "cap을 상자에 넣을 수 없습니다."와 같은 정책 측면은 나중에 생각납니다. 따라서 유사한 작업을 수행할 수 있지만 접근 방식이 다릅니다. 기능의 핵심은 순전히 설명적이라고 말합니다.
좋습니다. 확실히 감사합니다. 네, 제 질문도 다소 비슷합니다. 따라서 레오가 묻는 내용을 다시 말씀드리겠습니다. 예제에서 기능이 항상 무언가에 매개변수로 전달되는 경우였고 예제에서 기능이 저장되거나 반환되는 경우는 없었습니다. 따라서 기능을 1급 값으로 만드는 대신 암시적 및 종속 타입의 높은 대가를 치르지 않아도 되는 2급 것으로 모델링하는 것이 가능할까요? 레오가 지역 종류의 것으로 언급했던 것 같습니다.
실제로 시도되었습니다. 2016년 OOPSLA, 2급 값이었습니다. 제 생각에는 Purdue의 도달 가능성 작업의 전신이었습니다. 매우 제한적이며 실제로 완전한 예제는 아니지만 이러한 기능이 나타나는 예제를 보여드렸고 이것은 반복자에 대한 변환입니다. 따라서 반복자가 있고 반복자에 대한 맵이 있습니다. 반복자에 대한 맵을 수행하면 새로운 반복자가 생기고 무엇에 매달리는지 말해야 합니다. 그렇지 않으면 작성할 수 없습니다. 기능에 매달리는 클로저를 반환하는 것은 작성할 수 없습니다. 하지만 여전히 2급 값, 2급 것에 매달릴 수 있습니다. 반드시 1급이어야 한다는 의미는 아닙니다. 아니면 2급일 수 있습니다. 전달할 수만 있습니다. 따라서 map을 작성하면 결과는 이 값을 생성하기 위해 약간 위로 올라갑니다. 따라서 2급 값으로는 그렇게 할 수 없습니다.
좋습니다. 따라서 우리가 하고 싶은 것은 기본적으로 지역에서, 범위가 있더라도 이러한 것들을 지역에서 구성하고 전체가 벗어나지 않도록 하는 것입니다. 모든 값이 아래로 내려가야 한다는 엄격한 규율을 갖는 대신 말이죠. 좋습니다.
감사합니다. 안녕하세요. MIT의 Adam Chlipala입니다. 다른 언어에 익숙한 사람들에게 순수 함수 화살표를 도입하는 방법에 대한 박수갈채를 받았습니다. 이를 통해 기본적으로 일부 하위 계산에서 기능이 잊혀진다는 것을 선언할 수 있었습니다. 제가 명확하지 않은 것은 일부 코드가 일부 기능을 삭제하지만 전부는 삭제하지 않는 경우, 예를 들어 async와 옵션이 있고 옵션 부분을 일시적으로 잊어버리는 경우, 올바른 타입 시스템 기능을 가지고 있는지 추적하고 적용하는 것입니다. 오디션 관점에서 얻는 데 문제가 있었습니다. 좋습니다. 순수 함수 타입을 작성하여 기능을 캡처하지 않았다는 사실을 기록하는 방법에 대해 이야기했습니다. 더 세분화하여 기본적으로 여러 모나드 내부에 있고 하위 계산을 위해 일부는 떠나지만 전부는 떠나지 않는 패턴을 지원하려는 경우 이 타입 시스템에서 지원됩니까?
지원되지만 좀 더 복잡해질 것입니다. 왜냐하면 그 경우 "좋습니다. 이것과 이것과 이것만 캡처할 수 있는 함수 타입입니다."와 같이 상한을 제공하는 고정 세트가 있거나 map 함수를 통해 전달할 수 있기 때문입니다. 따라서 "함수는 추가 매개변수로 일부 기능을 가져오는 함수를 가져옵니다."라고 말할 수 있습니다. map에서 제공됩니다. 따라서 네, 이 모든 것을 할 수 있지만 좀 더 복잡해집니다.
해당 함수를 사용하는 클라이언트 코드가 더 복잡해 보입니까? 아니면 라이브러리 구현의 복잡성일 뿐입니까?
타입 추론을 어느 정도 가정하는지에 따라 다릅니다. 아마도 잘 숨길 수 있을 것입니다. 네. 좋습니다. 네, 감사합니다. 하지만 날카로운 면이 있습니다. 오류 메시지에서 알 수 있듯이 문제가 발생하고 함수에 복잡한 타입이 있으면... 네, 시간이 초과되었기 때문에 마지막 질문만 받겠습니다. Clemens Grelck, 비엔나 대학교. 적어도 이것의 기초가 C++의 고유성 타입 지정과 어떻게 관련되는지 궁금합니다. 죄송합니다. 이 작업의 기초가 고유성 타입 지정과 어떻게 관련되는지요? 고유성 타입 지정? 네. 고유성 타입 지정은 고유성을 사용하는 프레임워크 중 하나입니다. 예를 들어, 제 이전 박사 과정 학생인 Philipp Haller는 기능을 가진 고유성 타입 시스템을 가지고 있었지만 해당 기능은 기본적으로 타입 시스템에 있었습니다. 따라서 타입 시스템이 추적하는 다른 것들이 있었습니다. 프로그램 값이 아니었습니다. 따라서 고유성 타입 지정과 프로그램 값에 대해 가장 가까운 게시된 것은 아마도 도달 가능성 타입일 것입니다. 그것들은 그것에 상당히 가깝습니다. 그들은 이 철학을 따르고 고유성 타입 지정과 같은 것에 적용합니다. 네. 좋습니다. 감사합니다. 네. 다시 한 번 마틴, 매우 흥미로운 강연 감사합니다. 감사합니다.