Làm thế nào để tôi làm cho thuật toán này lười hơn mà không lặp lại chính mình?


9

(Lấy cảm hứng từ câu trả lời của tôi cho câu hỏi này .)

Xem xét mã này (phải tìm phần tử lớn nhất nhỏ hơn hoặc bằng một đầu vào đã cho):

data TreeMap v = Leaf | Node Integer v (TreeMap v) (TreeMap v) deriving (Show, Read, Eq, Ord)

closestLess :: Integer -> TreeMap v -> Maybe (Integer, v)
closestLess i = precise Nothing where
  precise :: Maybe (Integer, v) -> TreeMap v -> Maybe (Integer, v)
  precise closestSoFar Leaf = closestSoFar
  precise closestSoFar (Node k v l r) = case i `compare` k of
    LT -> precise closestSoFar l
    EQ -> Just (k, v)
    GT -> precise (Just (k, v)) r

Điều này không phải là rất lười biếng. Khi GTvụ kiện được nhập vào, chúng tôi biết chắc chắn rằng giá trị trả lại cuối cùng sẽ là Justmột cái gì đó chứ không phải là Nothing, nhưng Justvẫn không có sẵn cho đến khi kết thúc. Tôi muốn làm cho cái này lười hơn để nó Justcó sẵn ngay khi GTvụ án được đưa vào. Trường hợp thử nghiệm của tôi cho điều này là tôi muốn Data.Maybe.isJust $ closestLess 5 (Node 3 () Leaf undefined)đánh giá Truethay vì chạm đáy. Đây là một cách tôi có thể nghĩ để làm điều này:

data TreeMap v = Leaf | Node Integer v (TreeMap v) (TreeMap v) deriving (Show, Read, Eq, Ord)

closestLess :: Integer -> TreeMap v -> Maybe (Integer, v)
closestLess _ Leaf = Nothing
closestLess i (Node k v l r) = case i `compare` k of
  LT -> closestLess i l
  EQ -> Just (k, v)
  GT -> Just (precise (k, v) r)
  where
    precise :: (Integer, v) -> TreeMap v -> (Integer, v)
    precise closestSoFar Leaf = closestSoFar
    precise closestSoFar (Node k v l r) = case i `compare` k of
      LT -> precise closestSoFar l
      EQ -> (k, v)
      GT -> precise (k, v) r

Tuy nhiên, bây giờ tôi đang lặp lại chính mình: logic cốt lõi bây giờ là cả hai closestLessvà trong precise. Làm thế nào tôi có thể viết điều này để nó lười biếng nhưng không lặp lại chính mình?

Câu trả lời:


4

Thay vì sử dụng các hàm bao rõ ràng, bạn có thể tận dụng hệ thống loại. Lưu ý rằng phiên bản precisesử dụng Maybecho đoạn mã đầu tiên của bạn:

precise :: Maybe (Integer, v) -> TreeMap v -> Maybe (Integer, v)
precise closestSoFar Leaf = closestSoFar
precise closestSoFar (Node k v l r) = case i `compare` k of
  LT -> precise closestSoFar l
  EQ -> Just (k, v)
  GT -> precise (Just (k, v)) r

gần như chính xác cùng một thuật toán với phiên bản precisekhông có Maybetừ đoạn mã thứ hai của bạn, có thể được viết trong Identityfunctor như:

precise :: Identity (Integer, v) -> TreeMap v -> Identity (Integer, v)
precise closestSoFar Leaf = closestSoFar
precise closestSoFar (Node k v l r) = case i `compare` k of
  LT -> precise closestSoFar l
  EQ -> Identity (k, v)
  GT -> precise (Identity (k, v)) r

Chúng có thể được hợp nhất thành một phiên bản đa hình trong Applicative:

precise :: (Applicative f) => f (Integer, v) -> TreeMap v -> f (Integer, v)
precise closestSoFar Leaf = closestSoFar
precise closestSoFar (Node k v l r) = case i `compare` k of
  LT -> precise closestSoFar l
  EQ -> pure (k, v)
  GT -> precise (pure (k, v)) r

Chính nó, điều đó không thực hiện được nhiều, nhưng nếu chúng ta biết rằng GTnhánh sẽ luôn trả về một giá trị, chúng ta có thể buộc nó chạy trong Identityfunctor, bất kể functor bắt đầu. Đó là, chúng ta có thể bắt đầu trong Maybefunctor nhưng lặp lại vào Identityfunctor trong GTnhánh:

closestLess :: Integer -> TreeMap v -> Maybe (Integer, v)
closestLess i = precise Nothing
  where
    precise :: (Applicative t) => t (Integer, v) -> TreeMap v -> t (Integer, v)
    precise closestSoFar Leaf = closestSoFar
    precise closestSoFar (Node k v l r) = case i `compare` k of
      LT -> precise closestSoFar l
      EQ -> pure (k, v)
      GT -> pure . runIdentity $ precise (Identity (k, v)) r

Điều này hoạt động tốt với trường hợp thử nghiệm của bạn:

> isJust $ closestLess 5 (Node 3 () Leaf undefined)
True

và là một ví dụ hay về đệ quy đa hình.

Một điều tốt đẹp khác về cách tiếp cận này từ quan điểm hiệu suất là các -ddump-simplchương trình cho thấy không có trình bao bọc hoặc từ điển. Tất cả đã bị xóa ở cấp độ loại với các chức năng chuyên biệt cho hai chức năng:

closestLess
  = \ @ v i eta ->
      letrec {
        $sprecise
        $sprecise
          = \ @ v1 closestSoFar ds ->
              case ds of {
                Leaf -> closestSoFar;
                Node k v2 l r ->
                  case compareInteger i k of {
                    LT -> $sprecise closestSoFar l;
                    EQ -> (k, v2) `cast` <Co:5>;
                    GT -> $sprecise ((k, v2) `cast` <Co:5>) r
                  }
              }; } in
      letrec {
        $sprecise1
        $sprecise1
          = \ @ v1 closestSoFar ds ->
              case ds of {
                Leaf -> closestSoFar;
                Node k v2 l r ->
                  case compareInteger i k of {
                    LT -> $sprecise1 closestSoFar l;
                    EQ -> Just (k, v2);
                    GT -> Just (($sprecise ((k, v2) `cast` <Co:5>) r) `cast` <Co:4>)
                  }
              }; } in
      $sprecise1 Nothing eta

2
Đây là một giải pháp khá tuyệt vời
luqui

3

Bắt đầu từ việc thực hiện không lười biếng của mình, trước tiên tôi đã tái cấu trúc preciseđể nhận Justlàm đối số và khái quát loại của nó theo đó:

data TreeMap v = Leaf | Node Integer v (TreeMap v) (TreeMap v) deriving (Show, Read, Eq, Ord)

closestLess :: Integer -> TreeMap v -> Maybe (Integer, v)
closestLess i = precise Just Nothing where
  precise :: ((Integer, v) -> t) -> t -> TreeMap v -> t
  precise _ closestSoFar Leaf = closestSoFar
  precise wrap closestSoFar (Node k v l r) = case i `compare` k of
    LT -> precise wrap closestSoFar l
    EQ -> wrap (k, v)
    GT -> precise wrap (wrap (k, v)) r

Sau đó, tôi đã thay đổi nó để làm wrapsớm và gọi nó idtrong GTtrường hợp:

data TreeMap v = Leaf | Node Integer v (TreeMap v) (TreeMap v) deriving (Show, Read, Eq, Ord)

closestLess :: Integer -> TreeMap v -> Maybe (Integer, v)
closestLess i = precise Just Nothing where
  precise :: ((Integer, v) -> t) -> t -> TreeMap v -> t
  precise _ closestSoFar Leaf = closestSoFar
  precise wrap closestSoFar (Node k v l r) = case i `compare` k of
    LT -> precise wrap closestSoFar l
    EQ -> wrap (k, v)
    GT -> wrap (precise id (k, v) r)

Điều này vẫn hoạt động chính xác như trước đây, ngoại trừ lợi ích của sự lười biếng thêm vào.


1
Có phải tất cả những idcái ở giữa Justvà cuối cùng (k,v)bị loại bỏ bởi trình biên dịch? có lẽ là không, các hàm được cho là mờ đục và bạn có thể (loại khả thi) được sử dụng first (1+)thay vì idcho tất cả các trình biên dịch biết. nhưng nó tạo ra một mã nhỏ gọn ... tất nhiên, mã của tôi là làm sáng tỏ và đặc tả kỹ thuật của bạn ở đây, với sự đơn giản hóa bổ sung (loại bỏ các ids). cũng rất thú vị làm thế nào loại tổng quát hơn đóng vai trò là một ràng buộc, một mối quan hệ giữa các giá trị liên quan (mặc dù không đủ chặt chẽ, với first (1+)việc được cho phép như wrap).
Will Ness

1
(contd.) đa hình của bạn preciseđược sử dụng ở hai loại, tương ứng trực tiếp với hai chức năng chuyên dụng được sử dụng trong biến thể dài dòng hơn. tương tác tốt đẹp đó. Ngoài ra, tôi sẽ không gọi CPS này, wrapkhông được sử dụng như một phần tiếp theo, nó không được xây dựng "bên trong", nó được xếp chồng lên nhau - bằng cách đệ quy - ở bên ngoài. Có lẽ nếu nó được sử dụng như là sự tiếp nối, bạn có thể thoát khỏi những điều không liên quan idđó ... btw chúng ta có thể thấy ở đây một lần nữa mô hình đối số chức năng cũ được sử dụng làm chỉ báo về những việc cần làm, chuyển đổi giữa hai khóa hành động ( Justhoặc id).
Will Ness

3

Tôi nghĩ rằng phiên bản CPS mà bạn tự trả lời là tốt nhất nhưng để hoàn thiện hơn, đây là một vài ý tưởng. (EDIT: Câu trả lời của Buhr bây giờ là hiệu quả nhất.)

Ý tưởng đầu tiên là loại bỏ bộ closestSoFartích lũy "", và thay vào đó, hãy để GTtrường hợp xử lý tất cả logic chọn giá trị ngoài cùng bên phải nhỏ nhất so với đối số. Trong hình thức này, GTtrường hợp có thể trực tiếp trả về Just:

closestLess1 :: Integer -> TreeMap v -> Maybe (Integer, v)
closestLess1 _ Leaf = Nothing
closestLess1 i (Node k v l r) =
  case i `compare` k of
    LT -> closestLess1 i l
    EQ -> Just (k, v)
    GT -> Just (fromMaybe (k, v) (closestLess1 i r))

Điều này đơn giản hơn, nhưng chiếm thêm một chút không gian trên ngăn xếp khi bạn gặp rất nhiều GTtrường hợp. Về mặt kỹ thuật, bạn thậm chí có thể sử dụng nó fromMaybeở dạng tích lũy (nghĩa là thay thế fromJustẩn trong câu trả lời của luqui), nhưng đó sẽ là một nhánh dư thừa, không thể truy cập được.

Một ý tưởng khác cho rằng thực sự có hai "giai đoạn" của thuật toán, một trước và một sau khi bạn nhấn a GT, vì vậy bạn tham số hóa nó bằng một boolean để biểu diễn hai giai đoạn này và sử dụng các loại phụ thuộc để mã hóa bất biến sẽ luôn luôn có một kết quả trong giai đoạn thứ hai.

data SBool (b :: Bool) where
  STrue :: SBool 'True
  SFalse :: SBool 'False

type family MaybeUnless (b :: Bool) a where
  MaybeUnless 'True a = a
  MaybeUnless 'False a = Maybe a

ret :: SBool b -> a -> MaybeUnless b a
ret SFalse = Just
ret STrue = id

closestLess2 :: Integer -> TreeMap v -> Maybe (Integer, v)
closestLess2 i = precise SFalse Nothing where
  precise :: SBool b -> MaybeUnless b (Integer, v) -> TreeMap v -> MaybeUnless b (Integer, v)
  precise _ closestSoFar Leaf = closestSoFar
  precise b closestSoFar (Node k v l r) = case i `compare` k of
    LT -> precise b closestSoFar l
    EQ -> ret b (k, v)
    GT -> ret b (precise STrue (k, v) r)

Tôi đã không nghĩ câu trả lời của mình là CPS cho đến khi bạn chỉ ra. Tôi đã nghĩ về một cái gì đó gần hơn với một biến đổi bao bọc công nhân. Tôi đoán Raymond Chen lại đình công!
Joseph Sible-Phục hồi lại

2

Làm thế nào về

GT -> let Just v = precise (Just (k,v) r) in Just v

?


Bởi vì đó là một mô hình không hoàn chỉnh phù hợp. Ngay cả khi chức năng của tôi là toàn bộ, tôi không thích các phần của nó là một phần.
Joseph Sible-Phục hồi Monica

Vì vậy, bạn nói "chúng tôi biết chắc chắn" vẫn còn nghi ngờ. Có lẽ đó là khỏe mạnh.
luqui

Chúng tôi biết chắc chắn, vì khối mã thứ hai trong câu hỏi của tôi luôn trả về Justnhưng vẫn là tổng số. Tôi biết rằng giải pháp của bạn như được viết trên thực tế là hoàn toàn, nhưng nó dễ vỡ ở chỗ một sửa đổi dường như an toàn có thể dẫn đến việc chạm đáy.
Joseph Sible-Phục hồi Monica

Điều này cũng sẽ làm chậm chương trình một chút, vì GHC không thể chứng minh rằng nó sẽ luôn Justnhư vậy, vì vậy nó sẽ thêm một bài kiểm tra để đảm bảo rằng nó không phải Nothingmỗi khi nó lặp lại.
Joseph Sible-Phục hồi Monica

1

Không chỉ chúng ta luôn biết Just, sau lần khám phá đầu tiên, chúng ta cũng luôn biết Nothing cho đến lúc đó. Đó thực sự là hai "logic" khác nhau.

Vì vậy, chúng tôi đi bên trái trước hết, vì vậy hãy nóirằng :

data TreeMap v = Leaf | Node Integer v (TreeMap v) (TreeMap v) 
                 deriving (Show, Read, Eq, Ord)

closestLess :: Integer 
            -> TreeMap v 
            -> Maybe (Integer, v)
closestLess i = goLeft 
  where
  goLeft :: TreeMap v -> Maybe (Integer, v)
  goLeft n@(Node k v l _) = case i `compare` k of
          LT -> goLeft l
          _  -> Just (precise (k, v) n)
  goLeft Leaf = Nothing

  -- no more maybe if we're here
  precise :: (Integer, v) -> TreeMap v -> (Integer, v)
  precise closestSoFar Leaf           = closestSoFar
  precise closestSoFar (Node k v l r) = case i `compare` k of
        LT -> precise closestSoFar l
        EQ -> (k, v)
        GT -> precise (k, v) r

Giá là chúng tôi lặp lại nhiều nhất một bước nhiều nhất một lần.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.