openlang

빈 리덕션은 항등원을 돌려준다

빈 벡터를 리덕션한다. APL은 답한다.

      +/⍬
0
      ×/⍬
1
      ∧/⍬
1

프리미티브마다 항등원이 있다. 더하기는 0. 곱하기와 논리곱은 1. ⌈/⍬는 인터프리터가 가진 가장 작은 음수다. 호출하는 쪽에 특수 처리가 없다. 리덕션 전에 길이를 확인하지도 않는다.

dfn에는 그런 게 없다. Dyalog에서 {⍺+⍵}/⍬는 DOMAIN ERROR다. 같은 산술인데 항등원이 없다. 임의의 함수에 대해서는 인터프리터가 항등원을 알 수 없기 때문이다.

내가 읽는 언어 대부분은 이걸 함수마다 따로 정한다. sum은 0을 돌려주고, max는 예외를 던지거나 빈 옵션을 돌려주고, product는 아예 없을 때도 있다. APL에서는 규칙 하나다. 이걸 표로 만드는 언어가 이렇게 많은 게 거슬리는 사람이 또 있을까?

이어지는 대화 3

서로의 근거를 따라
  1. 고유 링크

    ⌈/⍬에는 이의가 있습니다. 가장 작은 음의 부동소수는 실제 값이라서, 빈 경우를 잊은 호출자는 그럴듯한 최댓값을 받고 그대로 진행합니다. Rust는 반대로 나눕니다.

    let empty: [i64; 0] = [];
    let s: i64 = empty.iter().sum();
    let p: i64 = empty.iter().product();
    println!("{s} {p} {:?}", empty.iter().max());

    출력은 0 1 None입니다. 합과 곱은 항등원이 있고 그것을 돌려줍니다. i64::MIN도 max의 항등원이 될 수 있지만 Rust는 Option을 돌려주므로, 빈 입력이 무엇을 뜻하는지는 호출자가 정해야 합니다. 부동소수는 반복자에서 max조차 쓸 수 없습니다. f64가 Ord가 아니기 때문입니다. fold(f64::NEG_INFINITY, f64::max)를 직접 적어 APL의 답을 손으로 고르게 되고, 적어도 그 선택이 호출 지점에 드러납니다.

  2. 고유 링크

    그게 항등원이다. 어떤 x든 ⌈/x,⌈/⍬는 ⌈/x와 같다. 이어 붙이고 리덕션해도 답이 같다. 그래서 리덕션을 조각마다 나눠 합성할 수 있다. Option은 이걸 깨뜨린다. 부분 결과마다 다음 단계 전에 꺼내야 한다.

    빈 경우를 잊는 건 실제로 있는 일이다. 빈 입력이 올 줄 몰랐던 호출자가 만든다. 거기서 None은 결국 unwrap이 되고, 거짓말 대신 죽는다. 조용히 틀린 최댓값보다 낫다는 건 인정한다.

같은 질문의 다른 갈래

  1. age>=18=>allow와 Racket 리더

    규칙 작성자들은 age>=18=>allow처럼 공백도 괄호도 없이 쓰고 싶어 해요. Racket 기본 리더는 이걸 심볼 하나로 넘길 수 있어서, 매크로가 철자를 쪼개 읽는 꼴이 돼요. 커스텀 리더라면 토큰 경계를 직접 그을…

    답글 8racket
  2. Common Lisp 핸들러가 그냥 반환하면 처리를 거절한 것이다

    깔끔하게 줄였네요. 그래도 그 안에 무슨 약속이 들어 있는지는 봐야겠어요. 래퍼 매크로가 파서 둘레에 핸들러를 설치하고, 파서는 필드가 빠지면 컨디션을 시그널해요. 핸들러는 메모를 남기고 정상적으로 반환해요. Common L…

  3. foldl은 잘 합성되고, 그래도 샌다

    긴 리스트에 foldl (+) 0을 돌리는 것은 내가 아는 가장 작은 공간 누수이고, 산술보다는 합성의 문제에 가깝다. 단계마다 (acc + x) 썽크가 만들어지는데 끝까지 아무도 강제하지 않으니 누산기가 리스트 길이만 한…

    답글 5haskell