Можно ли написать структуру данных или структуры данных, которые представляют только закрытые термины на Haskell или на любом другом языке?

Используя нотацию Де Брёйна, можно определить лямбда-термины как:

data BTerm = BVar Int | BLam BTerm | BApp BTerm BTerm

Или используя обычные обозначения, data Term = Var String | Lam String Term | App Term Term

Эти два типа данных позволяют создавать как закрытые термины, так и термины, содержащие свободные переменные.

Можно ли определить тип данных, который позволяет создавать только закрытые термины. т.е. только такие термины, как: \ xx, \x. хх, \ х. \ у. ху, \ х. \ у. у, \ х. \ у. \ zz(ху)

3 ответа

Вы можете использовать GADT, чтобы сделать список свободных переменных пустым. Свободные переменные могут храниться в списке уровня типа. Ниже я решил использовать индексы Де Брёйна для представления переменных.

Начнем с определения того, как добавить два списка уровней типов:

      {-# LANGUAGE KindSignatures, DataKinds, TypeFamilies, TypeOperators,
    GADTs, ScopedTypeVariables, TypeApplications #-}
{-# OPTIONS -Wall #-}

import GHC.TypeLits
import Data.Proxy

-- Type level lists append
type family (xs :: [Nat]) ++ (ys :: [Nat]) :: [Nat] where
   '[]       ++ ys = ys
   (x ': xs) ++ ys = x ': (xs ++ ys)

Затем мы вычисляем свободные переменные \ t учитывая те из t.

      -- Adjust Debuijn indices under a lambda:
-- remove zeros, decrement positives
type family Lambda (xs :: [Nat]) where
   Lambda '[]       = '[]
   Lambda (0 ': xs) = Lambda xs
   Lambda (x ': xs) = x-1 ': Lambda xs

Наконец наш GADT:

      -- "BTerm free" represents a lambda term with free variables "free"
data BTerm (free :: [Nat]) where
   BVar :: KnownNat n => BTerm '[n]
   BLam :: BTerm free -> BTerm (Lambda free)
   BApp :: BTerm free1 -> BTerm free2 -> BTerm (free1 ++ free2)

Тип для закрытых условий теперь легко определить:

      -- Closed terms have no free variables
type Closed = BTerm '[]

Мы сделали. Напишем несколько тестов. Начнем с Show instance, чтобы иметь возможность напечатать термины.

      showBVar :: forall n. KnownNat n => BTerm '[n] -> String
showBVar _ = "var" ++ show (natVal (Proxy @n))

instance Show (BTerm free) where
   show t@BVar       = showBVar t
   show (BLam t)     = "\\ " ++ show t
   show (BApp t1 t2) = "(" ++ show t1 ++ ")(" ++ show t2 ++ ")"

А вот пара тестов:

      -- \x. \y. \z. z (x y)
-- Output: \ \ \ (var0)((var2)(var1))
test1 :: Closed
test1 = BLam (BLam (BLam (BApp z (BApp x y))))
   where
   z = BVar @0
   y = BVar @1
   x = BVar @2

-- \x. \y. x y (\z. z (x y))
-- Output: \ \ ((var1)(var0))(\ (var0)((var2)(var1)))
test2 :: Closed
test2 = BLam (BLam (BApp (BApp x' y') (BLam (BApp z (BApp x y)))))
   where
   z  = BVar @0
   y  = BVar @1
   x  = BVar @2
   y' = BVar @0
   x' = BVar @1

Если вы хотите закодировать его в какой-либо форме, близкой к произвольным лямбда-выражениям, я подозреваю, что для отслеживания текущей глубины лямбда потребуется некоторое программирование на уровне типов. Это затруднит объединение терминов, не зная во время компиляции, какие типы у этих терминов.

Но если вы не возражаете против чего-то эквивалентного, которое выглядит совершенно иначе, хорошо известно, что комбинаторы SKI эквивалентны лямбда-исчислению. А поскольку SKI не предлагает явных лямбда-выражений или вообще ссылок на переменные, нет способа закодировать незамкнутый терм.

Конечно, это даже реализовано как связанная библиотека , которая является обобщением представления индексов Де Брёйна с использованием (полиморфно) рекурсивного типа данных:

      data Var bound free
  = Bound bound
  | Free free

newtype Scope bound term a
  = Scope { runScope :: term (Var bound (term a)) }

data Term var
  = Var var
  | App (Term var) (Term var)
  | Lam (Scope () Term var)
  -- == Lam (Term (Var () (Term var)))

Тип терминов индексируется типом переменных в этих терминах, как связанных, так и свободных. При определении новой вложенной области в лямбда-терме мы меняем этот тип: для связанных переменных мы просто добавляем еще один уровень вложенности, здесь аннотированный с помощью ()дать простые индексы Де Брёйна; для бесплатных переменных мы просто передаем тип, но также добавляем уровень вложенности с помощью Term. Вложенность на уровне типа отражает количество уровней Де Брёйна, к которым могут относиться индексы.

Сейчас Term Voidэто тип терма без свободных переменных; он все еще может иметь связанные переменные в силу того факта, что рекурсия полиморфна: Lam (Scope (Var (Bound ()))) :: Term Void представляет (λx. x).

Этот метод работает на простом Haskell 98 (то есть без GADT), хотя есть преимущества добавления некоторых причудливых типов, например, я регулярно использую их для типизированных AST и статических типов проверки типов. Эд Кметт дал хороший обзор дизайна boundв школе Haskell. В этом пространстве есть связанные библиотеки, такие как unbound и unbound-generics.

Другие вопросы по тегам