Implemente listas perezosas, preferiblemente en un idioma que no conoce bien [cerrado]

21

Este es un buen ejercicio para ser más fluido en ese lenguaje de programación que has querido aprender, pero que solo has jugado ligeramente. Esto implica trabajar con objetos, usar o simular cierres y estirar el sistema de tipos.

Su tarea es escribir código para administrar listas perezosas, luego usarlo para implementar este algoritmo para generar números de Fibonacci:

Los ejemplos de código están en Haskell

let fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
 in take 40 fibs

Resultado:

[0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368,75025,121393,196418,317811,514229,832040,1346269,2178309,3524578,5702887,9227465,14930352,24157817,39088169,63245986]

La implementación de su lista perezosa debe cumplir con estas pautas:

  • Un nodo Lista es una de tres cosas:
    • Nil: lista vacía.
      []
    • Contras: un solo elemento, junto con una Lista de los elementos restantes:
      1 : [2,3,4,5]
      ( :es el operador de contras en Haskell)
    • Thunk: un cálculo diferido que produce un nodo Lista cuando es necesario.
  • Es compatible con las siguientes operaciones:
    • nil: construye una lista vacía.
    • contras - Construye una celda contras.
    • thunk: construye un Thunk, dada una función que no toma argumentos y devuelve Nil o Cons.
    • force - Dado un nodo Lista:
      • Si es Nil o Contras, simplemente devuélvalo.
      • Si es un Thunk, llame a su función para obtener un Nil o Contras. Reemplace el golpe con ese Nil o Contras, y devuélvalo.
        Nota: Reemplazar el thunk con su valor forzado es una parte importante de la definición de "perezoso" . Si se omite este paso, el algoritmo de Fibonacci anterior será demasiado lento.
    • vacío: comprueba si un nodo Lista es Nulo (después de forzarlo).
    • head (también conocido como "auto"): obtén el primer elemento de una lista (o haz un ataque si es Nil).
    • tail (también conocido como "cdr"): obtenga los elementos después del encabezado de una lista (o haga un ajuste si es Nil).
    • zipWith: dada una función binaria (p (+). ej. ) y dos listas (posiblemente infinitas), aplique la función a los elementos correspondientes de las listas. Ejemplo:
      zipWith (+) [1,2,3] [1,1,10] == [2,3,13]
    • take: dado un número N y una lista (posiblemente infinita), tome los primeros N elementos de la lista.
    • print: imprime todos los elementos de una lista. Esto debería funcionar de forma incremental cuando se le da una lista larga o infinita.
  • fibsse utiliza en su propia definición. Configurar la recursividad perezosa es un poco complicado; deberás hacer algo como esto:

    • Asignar un golpe para fibs. Déjalo en un estado ficticio por ahora.
    • Defina la función thunk, que depende de una referencia a fibs.
    • Actualiza el thunk con su función.

    Es posible que desee ocultar esta tubería definiendo una función fixque llame a una función de retorno de Lista con su propio valor de retorno. Considera tomar una siesta corta para que esta idea pueda surgir.

  • No se requiere el polimorfismo (la capacidad de trabajar con listas de cualquier tipo de elemento), pero vea si puede encontrar una manera de hacerlo que sea idiomática en su idioma.

  • No te preocupes por la gestión de la memoria. Incluso los idiomas con recolección de basura tienden a transportar objetos que nunca volverá a usar (por ejemplo, en la pila de llamadas), así que no se sorprenda si su programa pierde memoria mientras recorre una lista infinita.

Siéntase libre de desviarse ligeramente de estas pautas para acomodar los detalles de su idioma, o explorar enfoques alternativos para listas perezosas.

Reglas:

  • Elige un idioma que no conozcas bien. No puedo "exigir" esto, de ahí la etiqueta "sistema de honor". Sin embargo, los votantes pueden revisar su historial para ver en qué idiomas ha estado publicando.
  • No use el soporte integrado de la lista diferida de su idioma para hacer todo. Publicar algo sustancial o al menos interesante.

    • Haskell está prácticamente fuera. Es decir, a menos que haga algo como esto:

      data List a = IORef (ListNode a)
      data ListNode a = Nil | Cons !a !(List a) | Thunk !(IO (ListNode a))
      

      Nota: La evaluación no estricta de Haskell no está prohibida, pero la implementación de su lista perezosa no debería derivar su capacidad directamente de allí. De hecho, sería interesante ver una solución eficiente y puramente funcional que no requiera pereza.

    • Pitón:

      • No utilices itertools.
      • Los generadores están bien, pero si los usa, tendrá que encontrar alguna forma de memorizar valores forzados.
Joey Adams
fuente
¿Cuál debería ser el comportamiento al invocar zipWithdos listas de diferentes longitudes?
balpha
@balpha: Elegí el comportamiento de Haskells: si alguna de las listas es nula, devuelve nil.
FUZxxl
@balpha: En Haskell, zipWith se detiene cuando cualquiera de las listas se queda sin elementos. Por lo tanto, zipWith (+) [1,2,3,4,5] [0,0,0] == [1,2,3]. Sin embargo, esto no importa para el algoritmo de Fibonacci anterior, ya que ambos argumentos para zipWith son listas infinitas.
Joey Adams
Este desafío tenía una sorpresa oculta: debes hacer algo especial para implementarlo fibscorrectamente, ya que depende de sí mismo. Actualicé la pregunta para elaborar sobre la recursión perezosa. FUZxxl lo descubrió por sí mismo.
Joey Adams
¿Qué quiere decir con "trabajar incrementalmente" cuando imprime una gran lista?
Lowjacker

Respuestas:

6

Posdata

He jugado con PostScript antes , pero no diría que lo sé particularmente bien (de hecho, supongo que puedes contar la cantidad de personas en el mundo que realmente conocen PostScript con una mano).

Me desvié de su especificación en que la función que se usa para crear un thunk puede devolver otro thunk; forceseguirá evaluando hasta que el resultado sea a nilo a cons.

Las listas se implementan como diccionarios:

<< /type /nil >>

<< /type /cons
   /head someValue
   /tail someList >>

<< /type /thunk
   /func evaluationFunction >>

<< /type /dataThunk
   /func evaluationFunction
   /data someValueToBePassedToTheFunction >>

El código sigue. Tenga en cuenta que estamos sobrescribiendo algunos operadores incorporados (en particular print; no he verificado si hay más); en el uso en el mundo real, esto debería ser vigilado. Por supuesto, no habrá uso en el mundo real, así que está bien.

Los comentarios antes de los procedimientos deben leerse como

% before2 before1 before0  <| procedure |>  after1 after0

es decir, mostrar el contenido esperado de la pila antes de la llamada y el contenido resultante de la pila después de la llamada. Los comentarios dentro de los procedimientos muestran el contenido de la pila después de que se haya ejecutado la línea particular.

% Helper procedure that creates a dictionary with the top two elements as keys
% and the next two elements as values.
%
% value1 value2 key1 key2  <| _twodict |>  << /key1 /value1 /key2 /value2 >>
/_twodict {
    << 5 1 roll    % << value1 value2 key1 key2
    4 2 roll       % << key1 key2 value1 value2
    3 2 roll       % << key1 value1 value2 key2
    exch >>
} def

/nil {
    << /type /nil >>
} def

% item list  <| cons |>  consCell
/cons {
    /head /tail _twodict
    dup /type /cons put
} def

% constructs a thunk from the function, which will be called with no
% arguments to produce the actual list node. It is legal for the function
% to return another thunk.
%
% func  <| thunk |>  lazyList
/thunk {
    /thunk /func /type _twodict
} def

% A dataThunk is like a regular thunk, except that there's an additional
% data object that will be passed to the evaluation function
%
% dataObject func  <| dataThunk |>  lazyList
/dataThunk {
    /data /func _twodict
    dup /type /dataThunk put 
} def

% lazyList  <| force |>  consOrNil
/force {
    dup /type get dup
    /thunk eq
    {
        pop
        dup /func get exec exch copy
        force
        dup /func undef
    }
    {
        /dataThunk eq
        {
            dup dup /data get exch
            /func get exec exch copy
            force
            dup dup /func undef /data undef
        } if
    } ifelse
} def

/empty {
    force
    /type get
    /nil eq
} def

/head {
    force /head get
} def

/tail {
    force /tail get
} def

/print {
    dup empty not
    {
        dup
        head ==
        tail
        print    
    }
    {
        pop
    } ifelse
} def

% sourceList n  <| take |>  resultingList
/take {
    /source /n _twodict
    {
        dup /source get exch    % source data
        /n get 1 sub dup        % source n-1 n-1
        -1 eq
        {
            pop pop nil
        }
        {                       % source n-1
            exch                % n-1 source
            dup head            % n-1 source head
            3 1 roll            % head n-1 source
            tail
            exch take           % head rest
            cons
        } ifelse
    }
    dataThunk
} def

% sourceList1 sourceList2 func  <| zipWith |>  resultList
/zipWith {
    3 1 roll
    2 array astore                  % func [L1 L2] 
    /func /sources _twodict
    {
        dup /sources get aload pop  % data L1 L2
        2 copy empty exch empty or
        {
            pop pop pop nil
        }
        {
            dup head exch tail      % data L1 H2 T2
            3 2 roll
            dup head exch tail      % data H2 T2 H1 T1
            exch                    % data H2 T2 T1 H1
            4 3 roll                % data T2 T1 H1 H2
            5 4 roll /func get      % T2 T1 H1 H2 func
            dup 4 1 roll            % T2 T1 func H1 H2 func
            exec                    % T2 T1 func NEWHEAD
            4 2 roll                % func NEWHEAD T2 T1
            exch 4 3 roll           % NEWHEAD T1 T2 func 
            zipWith cons
        } ifelse
    }
    dataThunk
} def

Cargue esto en Ghostscript, ignorando la página mostrada; solo estamos trabajando con el intérprete. Aquí está el algoritmo de Fibonacci:

[balpha@localhost lazylist]$ gs lazylist.ps 
GPL Ghostscript 8.71 (2010-02-10)
Copyright (C) 2010 Artifex Software, Inc.  All rights reserved.
This software comes with NO WARRANTY: see the file PUBLIC for details.
GS> /fibs 0 1 { fibs fibs tail { add } zipWith } thunk cons cons def
GS> fibs 40 take print
0
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765
10946
17711
28657
46368
75025
121393
196418
317811
514229
832040
1346269
2178309
3524578
5702887
9227465
14930352
24157817
39088169
63245986
GS>

Dos funciones interesantes adicionales:

% creates an infinite list that starts with the given value, incrementing
% by one for each additional element
%
% startValue  <| count |>  lazyList
/count {
    {
        dup
        1 add count
        cons
    }
    dataThunk
} def    

% apply the given function to each element of the source list, creating
% a (lazy) list that contains the corresponding results
%
% sourceList function  <| map |> resultList
/map {
    /source /func _twodict
    {
        dup /func get exch
        /source get                 % func source
        dup empty not
        {
            dup head                % func source head
            2 index                 % func source head func
            exec 3 1 roll           % newHead func source
            tail exch map cons
        }
        {
            pop pop nil
        } ifelse
    }
    dataThunk
} def

Comience a contar en 5, multiplique cada elemento de la lista resultante con 3 y muestre los primeros diez valores:

GS> 5 count { 3 mul } map 10 take print
15
18
21
24
27
30
33
36
39
42

Con respecto al polimorfismo: aunque PostScript está fuertemente tipado, permite tipos arbitrarios como valores de diccionario, por lo que puede agregar lo que quiera:

GS> 1337 [ 42 3.14 ] << /key /value >> (Hello world) 3 count
GS<5> cons cons cons cons 10 take print
1337
[42 3.14]
-dict-
(Hello world)
3
4
5
6
7
8
GS>

Tenga en cuenta que los errores de tipo, por ejemplo, al intentar agregar cadenas a los números, solo sucederán en el momento de la evaluación:

GS> (some string) (another string) nil cons cons
GS<1> 13 27 nil cons cons
GS<2> { add } zipWith      % no error yet
GS<1> print
Error: /typecheck in --add--
balpha
fuente
Asombroso. (¿Cómo) forcememoriza los valores devueltos?
Joey Adams
@JoeyAdams: De hecho lo hace. Después de evaluar un thunk, el copyoperador copia el contenido de la versión evaluada en el original, sobrescribiendo /typey posiblemente estableciendo otros valores. Después de la evaluación de forma recursiva hasta que tengamos una nilo cons, también (a través de undef) elimina /funcy, en su caso, /data. El último paso no es estrictamente necesario ( /funcy /datasimplemente sería ignorado), pero dejar este paso fuera perdería aún más memoria :)
balpha
6

do

Soy un principiante total en C, este código es en realidad el primer elemento real que codifiqué en C. Se compila sin advertencias y funciona bien en mi sistema.

Cómo construir

Primero, obtenga el tarball de mi servidor . Incluye un archivo MAKE, así que solo ejecuta makepara construirlo y luego make runpara ejecutarlo. Luego, el programa imprime una lista de los primeros 93 números de Fibonacci. (Después del número 94, se desborda un entero de 64 bits sin signo)

Explicación

El núcleo de los programas es el archivo lazy-list.c. En el archivo de encabezado correspondiente, defino una estructura list, esa es nuestra lista perezosa. Se parece a esto:

enum cell_kind {
  NIL,
  CONS,
  THUNK
};

typedef enum cell_kind cell_kind;

typedef long int content_t;

struct list {
  cell_kind kind;
  union {
    struct {
      content_t* head;
      struct list* tail;
    } cons;
    struct {
      struct list* (*thunk)(void*);
      /* If you want to give arguments to the thunk, put them in here */
      void* args;
    } thunk;
  } content;
};

El miembro kindes una especie de etiqueta. Marca, si buscamos las listas end ( NIL), una celda que ya está evaluada ( CONS) o un thunk ( THUNK). Luego, sigue una unión. Es

  • ya sea una celda ya evaluada con un valor y una cola
  • o un thunk, con un puntero de función y una estructura, que puede contener algunos argumentos a la función, si es necesario.

El contenido de la unión es afirmado por la etiqueta. Si la etiqueta es NIL, el contenido de la unión no está definido.

Al definir las funciones auxiliares mencionadas en la especificación anterior, generalmente se puede abstraer la definición de listas de su uso, por ejemplo. simplemente puede llamar nil()para obtener una lista vacía en lugar de crearla usted mismo.

Las tres funciones más interesantes son zipWith, takey fibonaccis. Pero no quiero explicar take, ya que es muy similar a zipWith. Todas las funciones, que operan perezosamente, tienen tres componentes:

  • Un envoltorio, que crea un golpe
  • Un trabajador, que realiza los cálculos para una celda.
  • Una estructura que mantiene los argumentos.

En caso de zipWith, estos son zipWith, __zipWithy __zipArgs. Solo los muestro aquí sin más explicaciones, su función debería ser bastante clara:

struct __zipArgs {
  content_t* (*f)(content_t*,content_t*);
  list* listA;
  list* listB;
};

static list* __zipWith(void* args_) {
  struct __zipArgs* args = args_;
  list* listA = args->listA;
  list* listB = args->listB;
  list* listC;

  content_t* (*f)(content_t*,content_t*) = args->f;
  content_t* headA = head(listA);
  content_t* headB = head(listB);
  content_t* headC;

  if (NULL == headA || NULL == headB) {
    free(args);
    return nil();
  } else {
    headC = f(headA, headB);
    args->listA = tail(listA);
    args->listB = tail(listB);
    listC = thunk(__zipWith,args);
    return cons(headC,listC);
  }
}

list* zipWith(content_t* (*f)(content_t*,content_t*),list* listA, list* listB) {
  struct __zipArgs* args = malloc(sizeof(struct __zipArgs));
  args->f = f;
  args->listA = listA;
  args->listB = listB;
  return thunk(__zipWith,args);
}

La otra función interesante es fibonaccis(). El problema es que necesitamos pasar un puntero de la primera y segunda celda al thunk de la tercera, pero para crear esas celdas también necesitamos un puntero al thunk. Para resolver ese problema, llené el puntero al thunk con un NULLprimero y lo cambié al thunk, después de que se creó. Aquí está la escucha:

static content_t* __add(content_t* a,content_t* b) {
  content_t* result = malloc(sizeof(content_t));
  *result = *a + *b;
  return result;
}

list* fibonaccis() {
  static content_t one_ = 1;
  static content_t zero_ = 0;
  list* one  = cons(&one_,NULL);
  list* two  = cons(&zero_,one);
  list* core = zipWith(__add,one,two);
  one->content.cons.tail = core;
  return two;

Posibles mejoras.

  • Mi solución no usa polimorfismo. Aunque posiblemente sea posible, mis habilidades en C no son suficientes para saber cómo usarlo. En cambio, usé un tipo content_t, que puede cambiar a lo que sea apropiado.
  • Uno podría extraer el thunk de la definición de la lista y usarlo solo de manera abstracta, pero hacerlo haría el código más complicado.
  • Uno podría mejorar las partes de mi código que no son buenas C.
FUZxxl
fuente
Buena presentación, especialmente por ser un primerizo C Con respecto al polimorfismo, si está dispuesto a asignar todo su contenido en el montón, puede usarlo void*como tipo de content_t.
Casey
@Casey: Muchas gracias. Pensé en usar void*también, pero pensé que eso evitaría el sistema de tipos demasiado. ¿No es posible usar plantillas?
FUZxxl
C no tiene plantillas, eso es C ++, pero sí, podría usar plantillas de C ++ para hacerlo genérico.
Casey
No se como usarlos. Pero supongo que es solo que C es un poco limitado en términos de su sistema de tipos. - Ni siquiera pude codificar este programa sin usar void*y amigos.
FUZxxl
1
"El miembro kindes una especie de etiqueta". Podrías llamarlo tag, ya que es un término bastante aceptado para el concepto (por ejemplo , unión etiquetada , máquina G sin etiqueta de Spineless . Por otro lado, "amable" tiene un significado diferente en un Contexto de Haskell: el tipo de un tipo. Int Tiene amable *, []tiene amable * -> *y (,)tiene amable * -> * -> *.
Joey Adams
5

C ++

Esta es la cosa más grande que he escrito en C ++. Normalmente uso Objective-C.

Es polimórfico pero nunca libera nada.

Mi mainfunción (y la addfunción para ZipWith) terminó luciendo así:

int add(int a, int b) {return a + b;}

int main(int argc, char **argv) {
    int numFib = 15; // amount of fibonacci numbers we'll print
    if (argc == 2) {
        numFib = atoi(argv[1]);
    }

    // list that starts off 1, 1...
    LazyList<int> fibo = LazyList<int>(new Cons<int>(1,
                     new LazyList<int>(new Cons<int>(1))));
    // zip the list with its own tail
    LazyList<int> *fiboZip = LazyList<int>::ZipWith(add, &fibo, fibo.Tail());
    // connect the begin list to the zipped list
    fibo.Tail() -> ConnectToList(fiboZip);

    // print fibonacci numbers
    int *fibonums = fibo.Take(numFib);    
    for (int i=0; i<numFib; i++) cout << fibonums[i] << " ";

    cout<<endl;

    return 0;
}

Esto da

 ./lazylist-fibo 20
 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 

Las clases funcionan así:

make a thunk:    LazyList<T>(new Thunk<T>( function, *args )) 
make empty list: LazyList<T>(new Nil<T>())
make cons:       LazyList<T>(new Cons<T>( car, *cdr ))

list empty:      list.Empty()
list's head:     list.Head()
list's tail:     list.Tail()
zipWith:         LazyList<T>::ZipWith(function, a, b)
take:            list.Take(n)
print:           list.Print()

Fuente completa: aquí . Es un desastre, principalmente porque está en un archivo grande.

Editar: cambió el enlace (el anterior estaba muerto).

marinus
fuente
3
Excelente trabajo, y gracias por tomar literalmente "lanzar un ajuste" :-) De ninguna manera soy un experto en C ++, pero una forma más de C ++ para implementar Thunk podría ser usar un objeto de función (también conocido como "functor") (que es decir, sobrecargar el ()operador) y usar la herencia para evitar tener que usarla void*. Vea aquí un ejemplo trivial de hacer eso.
Joey Adams
El enlace fuente completo está muerto ahora. ¿Podrías volver a subirlo? gist.github.com es un buen lugar para ponerlo.
Joey Adams
@JoeyAdams: hecho.
marinus
4

Pitón

No utiliza generadores para implementar la lista, solo para implementar el __iter__método para usar confor .

class Node(object):
    def __init__(self, head, tail):
        self.__head__ = head
        self.__tail__ = tail

    def force(self):
        return self

    def empty(self):
        return False

    def head(self):
        return self.__head__

    def tail(self):
        return self.__tail__

    def zip_with(self, func, other):
        def gen_func():
            if other.empty():
                return other
            return Node(func(self.head(), other.head()), self.tail().zip_with(func, other.tail()))
        return Thunk(gen_func)

    def __iter__(self):
        while not self.empty():
            yield self.head()
            self = self.tail()

    def append(self, other):
        while not self.tail().empty():
            self = self.tail()
        self.__tail__ = other

    def take(self, n):
        if n == 0:
            return NullNode()
        else:
            return Node(self.__head__, self.__tail__.take(n - 1))

    def _print(self):
        for item in self:
            print item

class NullNode(Node):
    def __init__(self):
        pass

    def empty(self):
        return True

    def head(self):
        raise TypeError("cannot get head of nil")

    def tail(self):
        raise TypeError("cannot get tail of nil")

    def zip_with(self, func, other):
        return self

    def append(self, other):
        raise TypeError("cannot append to nil")

    def take(self, n):
        return self

class Thunk(Node):
    def __init__(self, func):
        self.func = func

    def force(self):
        node = self.func()
        self.__class__ = node.__class__
        if not node.empty():
            self.__head__ = node.head()
            self.__tail__ = node.tail()
        return self

    def empty(self):
        return self.force().empty()

    def head(self):
        return self.force().head()

    def tail(self):
        return self.force().tail()

    def take(self, n):
        return self.force().take(n)

La lista de Fibonacci se crea así:

>>> from lazylist import *
>>> fib = Node(0, Node(1, NullNode()))
>>> fib.append(fib.zip_with(lambda a, b: a + b, fib.tail()))
>>> 
Lowjacker
fuente
1
Esto es hermoso. Mi línea favorita es self.__class__ = node.__class__. Tenga en cuenta que esto golpea una excepción NotImplemented cuando llega a 2971215073 (largo), que aparentemente es un argumento no válido para int .__ add__. Para admitir enteros grandes, hazlofib.append(fib.zip_with(lambda a,b: a+b, fib.tail()))
Joey Adams,
1
¿Por qué no puedes agregar al vacío o al thunk?
PyRulez
4

Rubí

Mi primer programa Ruby. Representamos todos los nodos como matrices, donde la longitud de la matriz determina el tipo:

0: empty list
1: thunk (call the single element to get the cons cell)
2: cons cell (1st is head, 2nd is tail)

El código es bastante sencillo, con un truco para restablecer la función thunk para configurar la fibra recursiva.

def nil_()
  return Array[]
end

def cons(a, b)
  return Array[a, b]
end

def thunk(f)
  return Array[f]
end

def force(x)
  if x.size == 1
    r = x[0].call
    if r.size == 2
      x[0] = r[0]
      x.push(r[1])
    else
      x.pop()
    end
  end
end

def empty(x)
  force(x)
  return x.size == 0
end

def head(x)
  force(x)
  return x[0]
end

def tail(x)
  force(x)
  return x[1]
end

def zipWith(f, a, b)
  return thunk(lambda {
    if empty(a) or empty(b)
      return nil_()
    else
      return cons(f.call(head(a), head(b)), zipWith(f, tail(a), tail(b)))
    end
  })
end

def take(n, x)
  if n == 0
    return nil_()
  else
    return cons(head(x), take(n - 1, tail(x)))
  end
end

def print(x)
  while not empty(x)
    puts x[0]
    x = x[1]
  end
end

def add(x, y)
  return x + y
end

T=thunk(nil)  # dummy thunk function
fibs=cons(0, cons(1, T))
T[0]=zipWith(method(:add), fibs, tail(fibs))[0]  # overwrite thunk function

print(take(40, fibs))
Keith Randall
fuente
Puedes usar en [...]lugar de Array[...].
Lowjacker
3

Google Go

Relativamente un nuevo idioma, y lo aprendí por CTRL+Fing del Spec .

package main
import "fmt"

type List struct {
  isNil, isCons, isThunk bool
  head *interface { }
  tail *List
  thunk (func() List)
}

func Nil() List {
  return List { true, false, false, nil, nil, Nil }
}

func Cons(a interface { }, b List) List {
  return List { false, true, false, &a, &b, Nil }
}

func Thunk(f(func() List)) List {
  return List { false, false, true, nil, nil, f }
}

func Force(x List) List {
  if x.isNil { return Nil()
  } else if x.isCons { return Cons(*x.head, *x.tail) }
  return Force(x.thunk())
}

func Empty(x List) bool {
  return Force(x).isNil;
}

func Head(x List) interface { } {
  y := Force(x)
  if y.isNil { panic("No head for empty lists.") }
  return *y.head
}

func Tail(x List) List {
  y := Force(x)
  if y.isNil { panic("No tail for empty lists.") }
  return *y.tail
}

func Take(n int, x List) List {
  if (n == 0) { return Nil() }
  return Thunk(func() List {
    y := Force(x)
    return Cons(*y.head, Take(n - 1, *y.tail))
  })
}

func Wrap(x List) List {
  return Thunk(func() List {
    return x
  })
}

func ZipWith(f(func(interface { }, interface { }) interface { }), a List, b List) List {
  return Thunk(func() List {
    x, y := Force(a), Force(b)
    if x.isNil || y.isNil {
      return Nil()
    }
    return Cons(f(*x.head, *y.head), ZipWith(f, *x.tail, *y.tail))
  });
}

func FromArray(a []interface { }) List {
  l := Nil()
  for i := len(a) - 1; i > -1; i -- {
    l = Cons(a[i], l)
  }
  return l
}

func Print(x List) {
  fmt.Print("[")
  Print1(x)
  fmt.Print("]")
}

func Print1(x List) {
  y := Force(x)
  if y.isCons {
    fmt.Print(Head(y))
    z := Force(Tail(y))
    if z.isCons { fmt.Print(", ") }
    Print1(z)
  }
}

func Plus(a interface { }, b interface { }) interface { } {
  return a.(int) + b.(int)
}

func Fibs() List {

  return Thunk(func() List {
    return Cons(0, Cons(1, Thunk(func() List {
      return ZipWith(Plus, Thunk(Fibs), Tail(Thunk(Fibs)))
    })))
  })
}

func Fibs0() List {
  // alternative method, working
  return Cons(0, Cons(1, Fibs1(0, 1)))
}

func Fibs1(a int, b int) List {
  c := a + b
  return Cons(c, Thunk(func() List { return Fibs1(b, c) }))
}

func CountUp(x int, k int) List {
  return Cons(x, Thunk(func() List {
    return CountUp(x + k, k)
  }))
}

func main() {
  //a := []interface{} { 0, 1, 2, 3 }
  //l, s := FromArray(a), FromArray(a)
  Print(Take(40, Fibs()))
}

El problema se solucionó al tratar con thunk-inside-a-thunks. Sin embargo, parece que el compilador en línea no puede tomar 40 elementos, tal vez debido a la memoria. Lo probaré en mi Linux más tarde.

[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368runtime: address space conflict: map() = 
throw: runtime: address space conflict

panic during panic

Probé el código con el compilador en línea , porque no puedo instalar Go en Windows fácilmente.

Ming-Tang
fuente
Esto es bastante agradable y simple. Sin embargo, en lugar de 3 bools, podría usar una sola etiqueta cuyos valores posibles son constantes generadas por el iotagenerador constante. Vea un ejemplo en la Especificación del lenguaje de programación Go y una respuesta en StackOverflow .
Joey Adams
Su Fibsfunción no funciona porque Go utiliza una evaluación estricta y se Fibsrepite sin una condición de terminación. Fibs0/ Fibs1utiliza un enfoque generador simple en lugar del algoritmo descrito en mi publicación, por lo que no cumple con los "requisitos". Actualicé mi publicación para elaborar sobre la recursividad perezosa, que es necesaria para implementar fibs = 0 : 1 : zipWith (+) fibs (tail fibs) .
Joey Adams
Cons(0, Cons(1, ZipWith(Plus, Thunk(Fibs), Tail(Thunk(Fibs))))), se queda sin memoria
Ming-Tang
Lo intenté Cons(0, Cons(1, Thunk(func() List { return ZipWith(Plus, Thunk(Fibs), Thunk(func() List { return Tail(Fibs()) })) })))y recibo un error de dirección de memoria no válida
Ming-Tang
1
Puesto que todavía está aprendiendo Go: Usted puede hacer algo de código mucho más elegante que esto utilizando interfaces para las listas y tipos separados para Thunks, etc.
cthom06
3

Cristal

A pesar de seguir el repositorio de GitHub, nunca he usado Crystal hasta ahora. Crystal es una variante Ruby de tipo estático con inferencia de tipo completo. Aunque ya hay una respuesta de Ruby, el tipeo estático de Crystal me llevó a usar el polimorfismo, en lugar de una matriz, para representar los nodos. Debido a que Crystal no permite la modificación de self, creé una clase de contenedor, llamada Node, que envolvería todo lo demás y administraría los thunks.

Junto con las clases, he creado las funciones constructoras lnil, consy thunk. Nunca antes he usado Ruby para más de un guión de 20 líneas, por lo que las cosas del bloque me desconcertaron bastante.

Basé la fibfunción en la respuesta Go .

class InvalidNodeException < Exception
end

abstract class LazyValue
end

class LNil < LazyValue
    def empty?
        true
    end

    def force!
        self
    end

    def head
        raise InvalidNodeException.new "cannot get head of LNil"
    end

    def tail
        raise InvalidNodeException.new "cannot get tail of Nil"
    end

    def take(n)
        Node.new self
    end
end

class Cons < LazyValue
    def initialize(@car, @cdr)
    end

    def empty?
        false
    end

    def force!
        @cdr.force!
        self
    end

    def head
        @car
    end

    def tail
        @cdr
    end

    def take(n)
        Node.new n > 0 ? Cons.new @car, @cdr.take n-1 : LNil.new
    end
end

class Thunk < LazyValue
    def initialize(&@func : (-> Node))
    end

    def empty?
        raise Exception.new "should not be here!"
    end

    def force!
        @func.call()
    end

    def head
        self.force!.head
    end

    def tail
        self.force!.tail
    end

    def take(n)
        self.force!.take n
    end
end

class Node
    def initialize(@value = LNil.new)
    end

    def empty?
        self.force!
        @value.empty?
    end

    def force!
        @value = @value.force!
        self
    end

    def head
        self.force!
        @value.head
    end

    def tail
        self.force!
        @value.tail
    end

    def take(n)
        self.force!
        return @value.take n
    end

    def print
        cur = self
        while !cur.empty?
            puts cur.head
            cur = cur.tail
        end
    end
end

def lnil
    Node.new LNil.new
end

def cons(x, r)
    Node.new Cons.new x, r
end

def thunk(&f : (-> Node))
    Node.new Thunk.new &f
end

def inf(st=0)
    # a helper to make an infinite list
    f = ->() { lnil }
    f = ->() { st += 1; cons st, thunk &f }
    thunk { cons st, thunk &f }
end

def zipwith(a, b, &f : Int32, Int32 -> Int32)
    thunk { a.empty? || b.empty? ? lnil :
            cons f.call(a.head, b.head), zipwith a.tail, b.tail, &f }
end

def fibs
    # based on the Go answer
    fibs2 = ->(a : Int32, b : Int32) { lnil }
    fibs2 = ->(a : Int32, b : Int32) { cons a+b, thunk { fibs2.call b, a+b } }
    cons 0, cons 1, thunk { fibs2.call 0, 1 }
end

fibs.take(40).print
zipwith(inf, (cons 1, cons 2, cons 3, lnil), &->(a : Int32, b : Int32){ a+b }).print
kirbyfan64sos
fuente
2

Doblé un poco las reglas porque todavía no hay una solución .NET aquí, o más generalmente una solución OOP, excepto la de Python que usa la herencia, pero es lo suficientemente diferente de mi solución para hacer que ambas sean interesantes (en particular desde Python permite modificar el self instancia, haciendo que la implementación de thunk sea sencilla).

Entonces esto es C # . Divulgación completa: no estoy cerca de un principiante en C #, pero no he tocado el idioma desde hace tiempo, ya que actualmente no lo uso en el trabajo.

Los puntos más destacados:

  • Todas las clases ( Nil, Cons, Thunk) se derivan de una clase base abstracta común List.

  • La Thunkclase usa el patrón Envelope-Letter . Básicamente, esto emula la self.__class__ = node.__class__asignación en la fuente de Python, ya que la thisreferencia no se puede modificar en C #.

  • IsEmpty, Heady Tailson propiedades.

  • Todas las funciones apropiadas se implementan de forma recursiva y perezosa (a excepción de Print, que no puede ser perezosa) devolviendo thunks. Por ejemplo, esto es Nil<T>.ZipWith:

    public override List<T> ZipWith(Func<T, T, T> func, List<T> other) {
        return Nil();
    }
    

    ... y esto es Cons<T>.ZipWith:

    public override List<T> ZipWith(Func<T, T, T> func, List<T> other) {
        return Thunk(() => {
            if (other.IsEmpty)
                return Nil();
    
            return Cons(func(Head, other.Head), Tail.ZipWith(func, other.Tail));
        });
    }
    

    Desafortunadamente, C # no tiene envío múltiple, de lo contrario también podría deshacerme de la ifdeclaración. Por desgracia, no hay dados.

Ahora, no estoy muy contento con mi implementación. Estoy feliz hasta ahora porque todo lo anterior es totalmente sencillo. Pero . Siento que la definición de Fibes innecesariamente complicada ya que necesito envolver los argumentos en thunks para que funcione:

List<int> fib = null;
fib = List.Cons(0, List.Cons(1,
    List.ZipWith(
        (a, b) => a + b,
        List.Thunk(() => fib),
        List.Thunk(() => fib.Tail))));

(Aquí List.Cons, List.Thunky List.ZipWithson envolturas de conveniencia).

Me gustaría entender por qué la siguiente definición mucho más fácil no funciona:

List<int> fib = List.Cons(0, List.Cons(1, List.Nil<int>()));
fib = fib.Concat(fib.ZipWith((a, b) => a + b, fib.Tail));

dada una definición apropiada de Concat, por supuesto. Esto es esencialmente lo que hace el código Python, pero no funciona (= lanzar un ajuste).

/ EDITAR: Joey ha señalado la falla obvia en esta solución. Sin embargo, reemplazar la segunda línea con un thunk también produce un error (Mono segfaults; sospecho que se desborda una pila que Mono no maneja bien):

fib = List.Thunk(() => fib.Concat(fib.ZipWith((a, b) => a + b, fib.Tail)));

El código fuente completo se puede encontrar como una esencia en GitHub .

Konrad Rudolph
fuente
"Desafortunadamente, C # no tiene envío múltiple": puede obtener el efecto mediante eventos, aunque eso es bastante hacky.
Peter Taylor
Lo irónico de la evaluación perezosa es que requiere un estado para su implementación. fib.ZipWithy fib.Tailusa lo viejo fib, que permanece [0,1]y no cambia. Por lo tanto, obtienes [0,1,1](creo), y tu Takefunción no te permite tomar de nulo (sin embargo, la toma de Haskell sí). Intente envolver el valor de la segunda línea en un thunk, para que se refiera al nuevo en fiblugar del antiguo.
Joey Adams
@Peter Sí; También puede usar el patrón Visitante para implementar el envío múltiple, pero quería que la solución fuera simple.
Konrad Rudolph
@Joey Duh. Es cegadoramente obvio ahora. Sin embargo, la solución thunk todavía no funciona (ver respuesta actualizada) pero ahora estoy demasiado ocupado para investigar.
Konrad Rudolph
2

Pico

Para el registro, esta solución utiliza una traducción de la fuerza de retardo del esquema como se define en srfi-45 . y crea listas perezosas además de eso.

{ 
` scheme's srfi-45 begins here `

  _lazy_::"lazy";
  _eager_::"eager";

  lazy(exp())::[[_lazy_, exp]];
  eager(exp)::[[_eager_, exp]];
  delay(exp())::lazy(eager(exp()));

  force(promise)::
    { content:promise[1];
      if(content[1]~_eager_,
        content[2],
        if(content[1]~_lazy_,
          { promise_:content[2]();
            content:promise[1];
            if(content[1]~_lazy_, 
             { content_:promise_[1];
               content[1]:=content_[1];
               content[2]:=content_[2];
               promise_[1]:=content });
            force(promise) })) };

` scheme's srfi-45 ends here `

nil:delay([]);
is_nil(s):size(force(s))=0;
cons(a(),b()):delay([a(),b()]);
head(s):force(s)[1];
tail(s):force(s)[2];

zipWith(f,a,b):
  lazy(if(is_nil(a)|is_nil(b),
         nil,
         cons(f(head(a),head(b)), zipWith(f,tail(a),tail(b)))));

fibs:void;
fibs:=cons(0, cons(1, zipWith(+,fibs,tail(fibs))));

take(c,s):
  lazy(if((c=0)|(is_nil(s)),
         nil,
         cons(head(s),take(c-1,tail(s)))));

print(s):
  { comma(s):
      if(is_nil(s),
        void,
        { display(head(s));
          if(!(is_nil(tail(s))), display(","));
          comma(tail(s)) });
    display("[");
    comma(s);
    display("]");
    void };

print(take(40,fibs))

}

las miradas de salida como esta: (pero dependiendo de cómo tpico. parcheado se podría tener más comillas dobles en ella display. normalmente imprime cadenas con comillas es decir, todas las apariciones de [, ,, ]tendría comillas alrededor de ellos como "[".)

[0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368,75025,121393,196418,317811,514229,832040,1346269,2178309,3524578,5702887,9227465,14930352,24157817,39088169,63245986]<void>

debido a los límites del tipo de datos entero en tpico, esto falla al calcular el 45º (o 46º desplazamiento) número de Fibonacci.

tenga en cuenta que tpico 2.0pl11 se divide en eso begin(a,b)(que comúnmente se escribe como {a;b}) y la iffunción no es recursiva de cola. sin mencionar que me llevó 5 años descubrir por qué beginno era recursiva la cola. También en ese momento escribí la traducción de srfi-45 en Pico. resultó ser que beginestaba esperando el valor de bantes de regresar cuando no necesitaba esperar. y una vez que lo conseguí, también pude arreglarlo ifya que tenía el mismo problema. y hubo este otro error que hizo que el constructor de nivel meta makeno funcionara.

Pico permite que una función controle si sus argumentos se evalúan antes de que se llame a la función o simplemente se empaquetan como thunks. para este código, puedo elipsis sobre las rarezas de la llamada por función .

Pico no tiene inferencia de tipo. Pensé en esto por un tiempo, pero me encontré con un problema debido a las rarezas de call by function . Se me ocurrió la afirmación de que los tipos deben codificar la existencia de nombres de variables enlazadas . pero estaba pensando principalmente en cómo adaptar la inferencia de tipo Hindley-Milner a un subconjunto de Pico sin mutación. La idea principal era que el verificador de tipos devuelve múltiples esquemas posibles si hay más de un enlace posible y el verificador de tipos tiene éxito si hay al menos un esquema de tipos posible . un posible esquema es aquel en el que ninguna asignación de tipo entra en conflicto.

Dan D.
fuente