데이터와 제어

함수와 제네릭

이름 있는 함수와 람다에서 시작해, 필요할 때만 1계층 제네릭과 제약 있는 제네릭으로 확장합니다.

함수는 반복해서 사용할 변환에 이름을 붙이고, 경계를 오가는 값의 타입을 선언합니다. 먼저 구체적인 이름 있는 함수로 시작하세요. 짧은 동작을 값처럼 전달할 때는 람다를 쓰고, 하나의 타입 관계를 여러 타입에서 유지해야 할 때만 타입 매개변수를 도입합니다.

한 프로그램에서 단계별 형태 실행하기

다음 코드를 functions-generics.tpz로 저장합니다.

TOPAZ
record Label derives Show {
    text: string,
}

function applyTwice(value: int, step: (int) -> int) -> int {
    step(step(value))
}

function firstOr<T>(values: Array<T>, fallback: T) -> T {
    match values {
        case [] => fallback
        case [first, ..] => first
    }
}

function showValue<T: Show>(value: T) -> string {
    Show.show(value)
}

let increment: (int) -> int = value => value + 1
let first: string = firstOr(["Topaz", "Classic"], "none")
let shown = showValue(Label { text: "docs" })

print("{applyTwice(3, increment)}")
print(first)
print(shown)

실행하기 전에 검사합니다.

BASH
topaz check functions-generics.tpz
topaz run functions-generics.tpz

결과는 다음과 같습니다.

출력
5
Topaz
Label { text: docs }

이름 있는 함수부터 시작하기

applyTwice는 두 매개변수와 int 결과를 선언합니다. 함수 몸체의 마지막 표현식이 결과가 되므로 여기서는 return을 따로 적지 않았습니다. 중간에 일찍 빠져나가는 흐름을 분명히 보여 주고 싶을 때는 return을 사용하세요.

step의 타입은 (int) -> int입니다. 호출자는 int를 받아 int를 돌려주는 함수를 제공해야 합니다. increment는 이 형태를 가진 람다입니다. 람다는 =>로 쓰며, 다른 값처럼 저장할 수 있고 클로저 규칙에 따라 주변 바인딩을 캡처합니다.

같은 관계가 반복될 때 타입 매개변수 추가하기

firstOr<T>는 배열 원소가 문자열인지 정수인지 다른 타입인지 알 필요가 없습니다. 배열 원소, 대체 값, 결과가 모두 같은 타입 T라는 관계만 약속합니다. 이 호출은 문자열을 넘기므로 추론 결과는 T = string입니다. 호출자가 타입 인수를 직접 적을 필요가 없습니다.

이것이 1계층 다형성입니다. 이름 있는 함수가 제네릭이고, 각 호출이 하나의 구체적인 타입을 선택합니다. 여러 타입에서 같은 알고리즘이 실제 타입 관계를 보존할 때 사용하세요. 타입 선택을 피하려는 목적으로 함수를 무조건 제네릭으로 만들 필요는 없습니다.

함수 몸체가 연산을 요구할 때만 제약 추가하기

showValue<T: Show>Show.show를 호출합니다. 따라서 함수 몸체에는 T가 정적 Show 프로토콜을 따른다는 근거가 필요합니다. Label derives Show가 그 명목 타입에 필요한 준수를 제공합니다. 단순히 출력할 수 있을 것처럼 보인다는 이유만으로 구조적 값이나 기본 타입이 자동으로 프로토콜을 따르지는 않습니다.

제약은 일반 제네릭 다음에 필요한 만큼만 추가하는 장치입니다. 함수 몸체가 실제로 두 프로토콜의 연산을 모두 사용한다면 이름 있는 제네릭 함수에 T: Show + JSON처럼 순서와 무관한 결합을 요구할 수 있습니다.

가장 작은 함수 형태 고르기

필요한 것선택독자에게 전달되는 의미
안정적인 이름을 가진 재사용 동작구체적인 이름 있는 함수매개변수와 결과가 공개 계약을 이룹니다.
값으로 전달하거나 저장할 짧은 동작람다주변 문맥이 구체적인 매개변수 타입을 제공합니다.
여러 타입에서 하나의 관계를 보존하는 알고리즘제약 없는 이름 있는 제네릭같은 T가 입력과 결과를 연결합니다.
정적 프로토콜 연산을 호출하는 제네릭 몸체제약 있는 이름 있는 제네릭몸체가 사용할 수 있는 연산을 제약에 정확히 적습니다.

호출, 기본값, 가변 인수

매개변수는 불변입니다. 기본값은 인수가 생략될 때 계산되며 리터럴이나 const 표현식이어야 합니다. 다른 매개변수를 참조할 수는 없습니다. 가변 인수 꼬리는 ...args: T로 쓰고 반드시 마지막에 둡니다. 함수 안에서는 Array<T>로 받습니다.

호출할 때는 호출 대상을 한 번 계산한 뒤 위치 인수와 스프레드 인수를 왼쪽에서 오른쪽으로 평가합니다. 이름 있는 인수는 그 뒤에 옵니다. 배열 스프레드는 가변 인수 꼬리에만 값을 보탤 수 있으며, 임의의 고정 매개변수를 채우는 용도로는 사용할 수 없습니다.

보통은 제네릭 타입을 추론합니다. 명시적 타입 인수를 허용하는 정확한 호출 형태에서는 목록을 빠짐없이 제공해야 하며, 값 인수와 기대 결과의 타입에도 맞아야 합니다. 일부만 장식처럼 적으면 정적 오류입니다.

흔한 실수: 제네릭 람다부터 찾기

이름 없는 짧은 동작은 주변 문맥에서 구체적인 매개변수 타입을 받는 람다로 작성합니다. 타입 매개변수 선언과 프로토콜 제약은 이름 있는 함수 선언에만 둡니다. 동작 자체에 재사용 가능한 제네릭 계약이 필요하다면 이름을 붙이고 그 계약을 한 번 선언하세요.

정확한 한계

  • 제네릭 함수는 1계층이며, 고위 함수 값은 아직 지원하지 않습니다.
  • 람다, 함수 타입, 별칭, 명목 선언, 수신자 메서드, 프로토콜 메서드에는 제약을 선언할 수 없습니다.
  • 제네릭 람다, 타입 매개변수 기본값, 분산(variance) 표기, 동적 프로토콜 디스패치, 임의의 트레이트 제약은 아직 지원하지 않습니다.
  • 제네릭 함수 몸체는 선언한 제약만 가지며, 더 강한 연산을 암묵적으로 사용할 수 없습니다.

첫 함수와 람다를 다시 보려면 값과 함수로 돌아가세요. 함수 타입과 제네릭 타입 형태는 타입, 캡처와 수명은 바인딩·스코프·클로저에서 이어집니다.