명령형 break 문 및 기타 루프 검사와 동등한 기능은 무엇입니까?


36

아래 논리가 있다고 가정 해 봅시다. 함수형 프로그래밍으로 작성하는 방법?

    public int doSomeCalc(int[] array)
    {
        int answer = 0;
        if(array!=null)
        {
            for(int e: array)
            {
                answer += e;
                if(answer == 10) break;
                if(answer == 150) answer += 100;
            }
        }
        return answer;
    }

대부분의 블로그, 기사의 예 ... 그냥 간단한 수학 함수 'Sum'의 간단한 경우를 설명합니다. 그러나 위와 비슷한 논리를 Java로 작성했으며 Clojure의 기능 코드로 마이그레이션하려고합니다. FP에서 위의 작업을 수행 할 수없는 경우 FP에 대한 프로모션 종류에이를 명시 적으로 명시하지 않습니다.

위의 코드는 완전히 필수적이라는 것을 알고 있습니다. 앞으로는 FP로 옮길 것을 미리 계획하지 않았습니다.


1
루프 내부에서 break와를 조합하여 return answer교체 할 수 있습니다 return. FP에서는 연속을 사용하여이 조기 수익을 구현할 수 있습니다. 예를 들어 en.wikipedia.org/wiki/Continuation
Giorgio

1
@Giorgio 연속은 여기서 엄청난 과잉이 될 것입니다. 어쨌든 다음 반복을 호출하려면 꼬리 호출을 수행하므로 더 이상 호출하지 않고 응답을 반환하면됩니다. 들어 중첩 사용하는 구조 조정에 코드를 연속성을 대신 상하로 움직이고을 사용할 수 있습니다 경우 루프, 또는 다른 복잡한 제어 흐름, 그건 항상 가능해야하지만, 지나치게 복잡한 코드 구조 될 수 있습니다 간단한 기술 (위의 어느 것 더 많거나 적은 해명 계속; 그리고 하나 이상의 출구 지점에는 반드시 필요합니다).
Will Ness

8
이 경우 : takeWhile.
Jonathan Cast

1
@ WillNess : 나는 단지 복잡한 계산을 떠나는 데 사용될 수 있기 때문에 언급하고 싶었습니다. 아마도 OP의 구체적인 예에 ​​가장 적합한 솔루션은 아닙니다.
Giorgio

@Giorgio 당신이 맞아요, 그것은 일반적으로 가장 포괄적입니다. 실제로이 질문은 매우 광범위합니다 (즉, 하트 비트로 SO에서 닫힙니다).
Will Ness

답변:


45

대부분의 기능적 언어에서 배열을 반복하는 것과 가장 가까운 것은 fold함수, 즉 배열의 각 값에 대해 사용자 지정 함수를 호출하고 누적 된 값을 체인을 따라 전달하는 함수입니다. 많은 기능적 언어에서 fold일부 조건이 발생하면 조기에 중지 할 수있는 옵션을 포함하여 추가 기능을 제공하는 다양한 추가 기능으로 보완됩니다. 게으른 언어 (예 : Haskell)에서 목록을 따라 더 이상 평가하지 않으면 간단히 중지 할 수있어 추가 값이 생성되지 않습니다. 따라서 예를 Haskell로 번역하면 다음과 같이 작성합니다.

doSomeCalc :: [Int] -> Int
doSomeCalc values = foldr1 combine values
  where combine v1 v2 | v1 == 10  = v1
                      | v1 == 150 = v1 + 100 + v2
                      | otherwise = v1 + v2

Haskell의 구문에 익숙하지 않은 경우이 줄을 세분화하면 다음과 같이 작동합니다.

doSomeCalc :: [Int] -> Int

int 목록을 허용하고 단일 int를 반환하여 함수의 유형을 정의합니다.

doSomeCalc values = foldr1 combine values

함수의 본문 : given argument values, foldr1arguments combine(아래 정의)와 함께 호출 된 return values. foldr1스크롤 프리미티브의 변형은 그리스트의 첫번째 값 어큐뮬레이터 세트로 시작한다 (따라서 1함수 이름), 그것은 보통이라고 왼쪽에서 오른쪽 기능 지정된 사용자 (사용 결합 오른쪽 배 , 따라서 r함수 이름에서). 그래서 foldr1 f [1,2,3]동등 f 1 (f 2 3)(또는 그 f(1,f(2,3))이상의 종래의 C 형 구문).

  where combine v1 v2 | v1 == 10  = v1

정의하는 combine지역 기능 : 두 개의 인수를 수신 v1하고 v2. 때 v110이다, 그냥 돌려줍니다 v1. 이 경우 v2는 평가되지 않으므로 루프가 여기서 중지됩니다.

                      | v1 == 150 = v1 + 100 + v2

또는 v1이 150이면 100을 추가하고 v2를 추가합니다.

                      | otherwise = v1 + v2

그리고 이러한 조건 중 어느 것도 맞지 않으면 v1을 v2에 추가하기 만하면됩니다.

결합 함수가 두 번째 인수를 평가하지 않으면 오른쪽 접힘이 종료된다는 사실은 Haskell의 게으른 평가 전략으로 인해 발생하기 때문에이 솔루션은 Haskell에 다소 고유합니다. Clojure를 모르지만 엄격한 평가를 사용한다고 생각하므로 fold표준 라이브러리에 조기 종료에 대한 특정 지원을 포함 하는 기능 이있을 것으로 기대합니다 . 이것은 종종 호출 foldWhile, foldUntil또는 유사한.

Clojure 라이브러리 문서를 간략히 살펴보면 이름 지정에서 대부분의 기능적 언어와 약간 다르며 fold원하는 것이 아닙니다 (병렬 계산을 가능하게하는 고급 메커니즘 임)이지만 reduce더 직접적입니다 동등한. reduced결합 함수 내 에서 함수를 호출 하면 조기 종료가 발생 합니다. 구문을 이해한다고 100 % 확신 할 수 없지만 찾고있는 것이 다음과 같은 것 같습니다.

(reduce 
    (fn [v1 v2]
        (if (= v1 10) 
             (reduced v1)
             (+ v1 v2 (if (= v1 150) 100 0))))
    array)

주의 : Haskell과 Clojure의 번역은이 특정 코드에 적합하지 않습니다. 그러나 그것들은 그것의 일반적인 요지를 전달합니다-이 예제들에 대한 특정한 문제들에 대해서는 아래 주석에서 논의를보십시오.


11
이름 v1 v2은 혼란 스럽습니다. v1"배열의 값"이지만 v2누적 된 결과입니다. 그리고 당신의 번역은 잘못된 것입니다. OP의 루프는 누적 된 (왼쪽에서) 값이 10의 배열에 도달하지 않고 종료 될 때 종료됩니다 . 여기에서 폴드를 사용하려면 왼쪽에있는 폴드를 일찍 종료하고 foldlWhile 여기에 약간의 변형을 사용 하십시오 .
Will Ness

2
그것은 이다 가장 잘못된 대답은 SE에 가장 upvotes을 얻는 방법 재미 .... 그것의 확인 당신에있어, 실수를하기 좋은 회사 : 도. 그러나 SO / SE의 지식 발견 메커니즘은 확실히 깨졌습니다.
Will Ness

1
Clojure 코드는 거의 정확하지만 조건은 (일명 )을 합산 (= v1 150)하기 전에 사용 합니다. v2e
NikoNyrh

1
Breaking this down line by line in case you're not familiar with Haskell's syntax-- 너는 나의 영웅이야. 하스켈은 저에게 미스터리입니다.
Captain Man

15
@WillNess 가장 즉각적으로 이해할 수있는 번역과 설명이기 때문에 공감됩니다. 그것이 잘못되었다는 사실은 부끄러운 일이지만 여기서 약간의 오류는 대답이 도움이된다는 사실을 부정하지 않기 때문에 상대적으로 중요하지 않습니다. 그러나 물론 수정해야합니다.
Konrad Rudolph

33

쉽게 재귀로 변환 할 수 있습니다. 그리고 꼬리 최적화 재귀 호출이 좋습니다.

의사 코드 :

public int doSomeCalc(int[] array)
{
    return doSomeCalcInner(array, 0);
}

public int doSomeCalcInner(int[] array, int answer)
{
    if (array is empty) return answer;

    // not sure how to efficiently implement head/tails array split in clojure
    var head = array[0] // first element of array
    var tail = array[1..] // remainder of array

    answer += head;
    if (answer == 10) return answer;
    if (answer == 150) answer += 100;

    return doSomeCalcInner(tail, answer);
}

14
네. 루프와 동등한 기능은 꼬리 재귀이며 조건부와 동등한 기능은 여전히 ​​조건부입니다.
Jörg W Mittag

4
@ JörgWMittag 오히려 꼬리 재귀가와 동등한 기능이라고 말하고 싶습니다 GOTO. Jules가 말했듯이 루프와 동등한 것은 적절한 접힘입니다.
leftaroundabout

6
@leftaroundabout 나는 실제로 동의하지 않습니다. 꼬리 재귀는 꼬리로만 이동할 필요가 있기 때문에 꼬리보다 더 제한적이라고 말하고 싶습니다. 루핑 구조와 기본적으로 동일합니다. 일반적으로 재귀는와 같습니다 GOTO. 어쨌든 꼬리 재귀를 컴파일 할 때 while (true)초기 반환이 break명령문 인 함수 본문 이있는 루프 로 대부분 비등 합니다. 폴드는 루프에 대해 정확하지만 실제로는 일반적인 루핑 구조보다 더 제한적입니다. 그것은 for-each 루프와 비슷합니다
J_mie6

1
@ J_mie6 내가 꼬리 재귀를 더 많이 고려하는 이유 GOTO는 어떤 상태에서 어떤 인수가 재귀 호출에 전달되는지 실제로 부지런히 예약해야하기 때문입니다. 괜찮은 방식으로 작성된 명령형 루프 (상태 변수가 무엇이며 각 반복에서 어떻게 변경되는지가 분명한 경우) 또는 순진한 재귀 (보통 인수로 많이 수행되지 않는 경우)와 같은 정도로 필요하지는 않습니다. 결과는 매우 직관적 인 방식으로 조립됩니다). ...
leftaroundabout

1
... 접기에 관해서는 : 당신이 옳습니다. 전통적인 폴드 (catamorphism)는 매우 특정한 종류의 루프이지만, 이러한 재귀 체계는 일반화 될 수 있습니다 (ana- / apo- / hylomorphisms). 총체적으로 이것들은 명령형 루프를 적절히 대체하는 IMO입니다.
leftaroundabout

13

나는 Jules의 답변을 정말로 좋아 하지만, 사람들이 게으른 함수형 프로그래밍에서 종종 놓친 부분을 지적하고 싶었습니다. 즉, 모든 것이 "루프 내부"일 필요는 없습니다. 예를 들면 다음과 같습니다.

baseSums = scanl (+) 0

offsets = scanl (\offset sum -> if sum == 150 then offset + 100 else offset) 0

zipWithOffsets xs = zipWith (+) xs (offsets xs)

stopAt10 xs = if 10 `elem` xs then 10 else last xs

result = stopAt10 . zipWithOffsets . baseSums

result [1..]         -- 10
result [11..1000000] -- 500000499945

로직의 각 부분을 별도의 함수로 계산 한 다음 함께 구성 할 수 있습니다. 이것은 일반적으로 문제 해결이 훨씬 쉬운 작은 기능을 허용합니다. 장난감 예제의 경우 아마도 이것이 제거하는 것보다 더 복잡해 지지만 실제 코드에서는 분리 된 함수가 종종 전체보다 훨씬 간단합니다.


논리가 여기 저기 흩어져 있습니다. 이 코드는 유지하기 쉽지 않습니다. 좋은 소비자 stopAt10아닙니다 . 당신의 답 당신이 인용 한 것보다 더 낫습니다. 그것이 기본적인 scanl (+) 0가치 생산자 를 정확하게 분리 시킨다는 것 입니다. 그들의 소비는 제어 로직을 직접 통합해야 하며 명시 적으로 두 개의 spans와 a로 더 잘 구현됩니다 last. 이는 원래 코드 구조와 논리를 밀접하게 따르며 유지 관리하기 쉽습니다.
Will Ness

6

대부분의 목록 처리 예제는 같은 사용 기능을 볼 수 map, filter, sum등 전체 목록에서 작동하는합니다. 그러나 귀하의 경우 조건부 조기 종료가 있습니다-일반적인 목록 작업에서 지원하지 않는 다소 드문 패턴입니다. 따라서 추상화 수준을 낮추고 재귀를 사용해야합니다. 이는 명령형 예제의 모양과 비슷합니다.

이것은 Clojure 로의 다소 직접적 (관용적이지 않은) 번역입니다.

(defn doSomeCalc 
  ([lst] (doSomeCalc lst 0))
  ([lst sum]
    (if (empty? lst) sum
        (if (= sum 10) sum
            (let [sum (+ sum (first lst))]
                 [sum (if (= sum 150) (+ sum 100) sum)]
               (recur (rest lst) sum))))))) 

편집 : 그 밖으로 쥘 포인트 reduceClojure의의는 어떻게 조기 종료를 지원합니다. 이것을 사용하는 것이 더 우아합니다.

(defn doSomeCalc [lst]  
  (reduce (fn [sum val]
    (if (= sum 10) (reduced sum)
        (let [sum (+ sum val)]
             [sum (if (= sum 150) (+ sum 100) sum)]
           sum))
   lst)))

어쨌든 명령형 언어에서와 같이 기능적 언어로 무엇이든 할 수 있지만 우아한 솔루션을 찾기 위해 사고 방식을 변경 해야하는 경우가 종종 있습니다. 명령형 코딩에서는 목록을 단계별로 처리하는 것을 생각하지만 기능적 언어에서는 목록의 모든 요소에 적용 할 연산을 찾습니다.


내 답변에 방금 추가 한 편집 내용을 참조하십시오. Clojure의 reduce작업은 조기 종료를 지원합니다.
Jules

@ 줄스 : 쿨-아마 더 관용적 솔루션입니다.
JacquesB

잘못되었거나 takeWhile'일반적인 작업'이 아닙니까?
Jonathan Cast

@jcast- takeWhile일반적인 작업이지만 중지 여부를 결정하기 전에 변환 결과가 필요하기 때문에이 경우 특히 유용하지 않습니다. 게으른 언어에서이 문제가되지 않습니다 : 당신이 사용할 수있는 scantakeWhile검사 결과에 (이 사용을하지 않는 동안 칼 Bielefeldt의 대답을 참조 takeWhile쉽게 그렇게 다시 작성할 수)을,하지만 Clojure에서와 같은 엄격한 언어에 대해이 것 전체 목록을 처리 한 다음 결과를 버리는 것을 의미합니다. 그러나 생성기 기능 으로이 문제를 해결할 수 있으며 클로저가 지원합니다.
Jules

take-whileClojure의 @Jules 는 (문서에 따라) 게으른 시퀀스를 생성합니다. 이 문제를 해결하는 또 다른 방법은 변환기를 사용하는 것입니다.
Will Ness

4

다른 답변에서 지적했듯이 Clojure는 reduced축소를 조기에 중지해야합니다.

(defn some-calc [coll]
  (reduce (fn [answer e]
            (let [answer (+ answer e)]
               (case answer
                 10  (reduced answer)
                 150 (+ answer 100)
                 answer)))
          0 coll))

특정 상황에 가장 적합한 솔루션입니다. 또한 결합에서 마일리지를 많이 얻을 수 있습니다 reduced와 함께 transduce당신의 트랜스 듀서를 사용하는 수, map, filter그것은 당신의 일반적인 질문에 대한 완전한 해답 거리가 멀다 그러나 등.

이스케이프 연속은 일반화 된 버전의 break 및 return 문입니다. 그것들은 일부 체계 ( call-with-escape-continuation), 공통 리스프 ( block+ return, catch+ throw) 및 심지어 C ( setjmp+ longjmp) 로 직접 구현됩니다 . 표준 구성표 또는 Haskell 및 Scala의 연속 모나드에서 볼 수있는보다 일반적인 구분 또는 무제한 연속을 이스케이프 연속으로 사용할 수도 있습니다.

예를 들어 라켓에서는 다음 let/ec과 같이 사용할 수 있습니다 .

(define (some-calc ls)
  (let/ec break ; let break be an escape continuation
    (foldl (lambda (answer e)
             (let ([answer (+ answer e)])
               (case answer
                 [(10)  (break answer)] ; return answer immediately
                 [(150) (+ answer 100)]
                 [else  answer])))
           0 ls)))

다른 많은 언어들도 예외 처리 형식의 이스케이프 연속과 같은 구문을 가지고 있습니다. Haskell에서는으로 다양한 오류 모나드 중 하나를 사용할 수도 있습니다 foldM. 그것들은 주로 초기 반품을 위해 예외 또는 오류 모나드를 사용하는 오류 처리 구문이기 때문에 일반적으로 문화적으로 받아 들일 수없고 상당히 느릴 수 있습니다.

고차 함수에서 테일 호출로 드롭 다운 할 수도 있습니다.

루프를 사용할 때 루프 바디의 끝에 도달하면 다음 반복으로 자동 입력됩니다. (또는 )로 다음 반복을 일찍 입력 continue하거나 루프를 종료 할 수 있습니다 . 꼬리 호출 (또는 꼬리 재귀를 모방하는 Clojure의 구성)을 사용할 때는 항상 다음 반복을 입력하기 위해 명시적인 호출을해야합니다. 루핑을 멈추려면 재귀 호출을하지 않고 값을 직접 제공하십시오.breakreturnloop

(defn some-calc [coll]
  (loop [answer 0, [e es :as coll] coll]
    (if (empty? coll)
      answer
      (let [answer (+ answer e)]
        (case answer
          10 answer
          150 (recur (+ answer 100) es)
          (recur answer es))))))

1
Haskell에서 오류 모나드를 다시 사용하면 실제 성능 저하가 있다고 생각하지 않습니다. 예외 처리 라인을 따라 생각하는 경향이 있지만 동일한 방식으로 작동하지 않으며 스택 워킹이 필요하지 않으므로이 방법을 사용하면 실제로 문제가되지 않습니다. 또한, 같은 것을 사용하지 않는 문화적 이유가 있더라도 MonadError기본적으로 동등한 Either것은 오류 처리에만 대한 편견이 없으므로 대체로 쉽게 사용할 수 있습니다.
Jules

@ 줄 (Jules) Left를 반환한다고해서 접기가 전체 목록 (또는 다른 순서)을 방문하는 것을 막을 수는 없습니다. 그러나 Haskell 표준 라이브러리 내부에 친숙하지 않습니다.
nilern

2

복잡한 부분은 루프입니다. 시작하겠습니다. 루프는 일반적으로 단일 함수로 반복을 표현하여 기능적 스타일로 변환됩니다. 반복은 루프 변수의 변환입니다.

다음은 일반적인 루프의 기능적 구현입니다.

loop : v -> (v -> v) -> (v -> Bool) -> v
loop init iter cond_to_cont = 
    if cond_to_cont init 
        then loop (iter init) iter cond
        else init

(루프 변수의 초기 값, [루프 변수에서] 단일 반복을 표현하는 함수) (루프를 계속하기위한 조건)를 취합니다.

귀하의 예제는 배열에서 루프를 사용하며, 이는 또한 중단됩니다. 명령형 언어의이 기능은 언어 자체로 구워집니다. 기능적 프로그래밍에서 이러한 기능은 일반적으로 라이브러리 수준에서 구현됩니다. 가능한 구현은 다음과 같습니다

module Array (foldlc) where

foldlc : v -> (v -> e -> v) -> (v -> Bool) -> Array e -> v
foldlc init iter cond_to_cont arr = 
    loop 
        (init, 0)
        (λ (val, next_pos) -> (iter val (at next_pos arr), next_pos + 1))
        (λ (val, next_pos) -> and (cond_to_cont val) (next_pos < size arr))

그것에서 :

외부에서 볼 수있는 루프 변수 와이 함수가 숨기는 배열의 위치를 ​​포함하는 ((val, next_pos)) 쌍을 사용합니다.

반복 함수는 일반 루프보다 약간 더 복잡합니다.이 버전에서는 배열의 현재 요소를 사용할 수 있습니다. [ 카레 형태입니다.]

이러한 기능은 일반적으로 "fold"라고합니다.

배열 요소의 누적이 왼쪽 연관 방식으로 수행됨을 나타 내기 위해 이름에 "l"을 넣습니다. 배열을 낮은 인덱스에서 높은 인덱스로 반복하는 명령형 프로그래밍 언어의 습관을 모방합니다.

이름에 "c"를 붙여이 버전의 접기가 루프를 조기에 중지할지 여부와시기를 제어하는 ​​조건을 취함을 나타냅니다.

물론 이러한 유틸리티 기능은 사용 된 기능적 프로그래밍 언어와 함께 제공되는 기본 라이브러리에서 쉽게 사용할 수 있습니다. 시연을 위해 여기에 썼습니다.

우리는 명령적인 경우에 언어로 된 모든 도구를 가지고 있으므로 예제의 특정 기능을 구현할 수 있습니다.

루프의 변수는 쌍입니다 ( 'answer', 계속할지 여부를 인코딩하는 부울 값).

iter : (Int, Bool) -> Int -> (Int, Bool)
iter (answer, cont) collection_element = 
  let new_answer = answer + collection_element
  in case new_answer of
    10 -> (new_answer, false)
    150 -> (new_answer + 100, true)
    _ -> (new_answer, true)

새로운 "변수" 'new_answer'를 사용했습니다. 함수형 프로그래밍에서는 이미 초기화 된 "변수"의 값을 변경할 수 없기 때문입니다. 성능에 대해 걱정하지 않아도됩니다. 컴파일러는 수명 분석을 통해 '신규 _ 응답'에 대한 '응답'의 메모리를 더 효율적으로 사용할 수 있습니다.

이것을 이전에 개발 한 루프 함수에 통합 :

doSomeCalc :: Array Int -> Int
doSomeCalc arr = fst (Array.foldlc (0, true) iter snd arr)

여기서 "배열"은 foldlc 함수를 내보내는 모듈 이름입니다.

"fist", "second"는 쌍 매개 변수의 첫 번째, 두 번째 구성 요소를 리턴하는 함수를 나타냅니다.

fst : (x, y) -> x
snd : (x, y) -> y

이 경우 "point-free"스타일은 doSomeCalc 구현의 가독성을 향상시킵니다.

doSomeCalc = Array.foldlc (0, true) iter snd >>> fst

(>>>)는 함수 구성입니다. (>>>) : (a -> b) -> (b -> c) -> (a -> c)

위와 동일하며 정의 방정식의 양쪽에서 "arr"매개 변수 만 제외됩니다.

마지막으로 : 대소 문자 확인 (배열 == null). 더 나은 디자인의 프로그래밍 언어에서는 기본 훈련이있는 나쁜 디자인의 언어에서도 선택적 유형 을 사용하여 존재하지 않는 것을 표현합니다. 이것은 함수형 프로그래밍과는 관련이 없으며 궁극적으로 문제는 처리되지 않습니다.


0

먼저 루프의 각 반복이 조기 종료되거나 answer정확히 한 번만 변경 되도록 루프를 약간 다시 작성하십시오 .

    public int doSomeCalc(int[] array)
    {
        int answer = 0;
        if(array!=null)
        {
            for(int e: array)
            {
                if(answer + e == 10) return answer + e;
                else if(answer + e == 150) answer = answer + e + 100;
                else answer = answer + e;
            }
        }
        return answer;
    }

이 버전의 동작은 이전과 정확히 동일하지만 이제 재귀 스타일로 변환하는 것이 훨씬 간단합니다. 직접 Haskell 번역은 다음과 같습니다.

doSomeCalc :: [Int] -> Int
doSomeCalc = recurse 0
  where recurse :: Int -> [Int] -> Int
        recurse answer [] = answer
        recurse answer (e:array)
          | answer + e == 10 = answer + e
          | answer + e == 150 = recurse (answer + e + 100) array
          | otherwise = recurse (answer + e) array

이제 순전히 기능적이지만 명시 적 재귀 대신 폴드를 사용하여 효율성과 가독성 관점에서 향상시킬 수 있습니다.

import Control.Monad (foldM)

doSomeCalc :: [Int] -> Int
doSomeCalc = either id id . foldM go 0
  where go :: Int -> Int -> Either Int Int
        go answer e
          | answer + e == 10 = Left (answer + e)
          | answer + e == 150 = Right (answer + e + 100)
          | otherwise = Right (answer + e)

이와 관련 Left하여, 가치가있는 조기 종료 및 가치가 Right있는 재귀를 계속합니다.


이제 다음과 같이 조금 더 단순화 할 수 있습니다.

import Control.Monad (foldM)

doSomeCalc :: [Int] -> Int
doSomeCalc = either id id . foldM go 0
  where go :: Int -> Int -> Either Int Int
        go answer e
          | answer' == 10 = Left 10
          | answer' == 150 = Right 250
          | otherwise = Right answer'
          where answer' = answer + e

이것은 최종 Haskell 코드보다 낫지 만 이제는 원래 Java로 다시 매핑되는 방법이 약간 명확하지 않습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.