Skip to content

Nim Tutorial 2 (Spanish)

Juan Carlos edited this page Jan 2, 2021 · 1 revision

Nim Tutorial (Part II)

Author: Andreas Rumpf
Translator: Durden Tyler
"La repetición hace que lo ridículo sea razonable". - Norman Wildberger

Este documento es un tutorial para las construcciones avanzadas de Nim. Tenga en cuenta que este documento es algo obsoleto ya que el manual contiene muchos más ejemplos del lenguaje.

Los pragmas son el método de Nim para dar al compilador información/comandos adicionales sin introducir una gran cantidad de nuevas palabras clave. Los pragmas son entre-corchetes especiales {. y .}. Este tutorial no cubre los pragmas. Consulte el manual o la guía del usuario para obtener una descripción de los pragmas.

Si bien el soporte de Nim para la programación orientada a objetos (OOP) es minimalista, se pueden utilizar potentes técnicas de programación orientada a objetos. OOP se ve como una forma de diseñar un programa, no la única forma. A menudo, un enfoque procedimental conduce a código más eficiente. En particular, preferir la composición a la herencia suele ser el mejor diseño.

La herencia en Nim es completamente opcional. Para habilitar la herencia con información del tipo de tiempo de ejecución del que el objeto necesita heredar RootObj. Esto se puede hacer directa o indirectamente heredando de un objeto que hereda de RootObj. Generalmente los tipos con herencia también se marcan como tipos ref aunque esto no se aplica estrictamente. Para comprobar en tiempo de ejecución si un objeto es de cierta tipo, se puede utilizar el operador of.

type
  Person = ref object of RootObj
    name*: string  # the * means that `name` is accessible from other modules
    age: int       # no * means that the field is hidden from other modules

  Student = ref object of Person # Student inherits from Person
    id: int                      # with an id field

var
  student: Student
  person: Person
assert(student of Student) # is true
# object construction:
student = Student(name: "Anton", age: 5, id: 2)
echo student[]

La herencia se realiza con la sintaxis de object of. La herencia múltiple actualmente no es compatible. Si un tipo de objeto no tiene un objeto base adecuado, RootObj se puede utilizar como su base, pero esto es solo una convención. Objetos que no tienen ningúna base es implícitamente final. Puede utilizar el pragma inheritable para introducir nuevas raíces de objetos además de system.RootObj. (Esto se usa en el wrapper GTK, por ejemplo)

Los objetos de referencia deben usarse siempre que se use la herencia. No es estrictamente necesario, pero con asignaciones de objetos no ref como let person: Person = Student(id: 123) truncará los campos de subclase.

Nota: La composición (es tiene-una) es a menudo preferible a la herencia (es-una) para la reutilización de código simple. Dado que los objetos son tipos por valor en Nim, la composición es tan eficaz como la herencia.

Los objetos, tuplas y referencias pueden modelar estructuras de datos bastante complejas que dependen unos de otros; son mutuamente recursivos. En Nim estos tipos solo pueden declararse dentro de una única sección de tipos. (Algo más requeriría una búsqueda anticipada de símbolos arbitraria que ralentiza la compilación).

Ejemplo:

type
  Node = ref object  # a reference to an object with the following field:
    le, ri: Node     # left and right subtrees
    sym: ref Sym     # leaves contain a reference to a Sym

  Sym = object       # a symbol
    name: string     # the symbol's name
    line: int        # the line the symbol was declared in
    code: Node       # the symbol's abstract syntax tree

Nim distingue entre Type Cast y Type Conversion. Las conversiones se realizan con el operador cast y obligan al compilador a interpretar un patrón de bits como de otro tipo.

Las conversiones de tipos son una forma mucho más educada de convertir un tipo en otro: Conservan el valor abstracto, no necesariamente el patrón de bits. Si un la conversión de tipo no es posible, el compilador se queja o hay una excepción.

La sintaxis para type conversion es destination_type(expression_to_convert):

proc getID(x: Person): int =
  Student(x).id

InvalidObjectConversionDefect sera la exception lanzada si x no es un Student.

A menudo, una jerarquía de objetos es excesiva en determinadas situaciones en las que se necesitan tipos variantes (Object Variant).

Un ejemplo:

# This is an example how an abstract syntax tree could be modelled in Nim
type
  NodeKind = enum  # the different node types
    nkInt,          # a leaf with an integer value
    nkFloat,        # a leaf with a float value
    nkString,       # a leaf with a string value
    nkAdd,          # an addition
    nkSub,          # a subtraction
    nkIf            # an if statement
  Node = ref object
    case kind: NodeKind  # the ``kind`` field is the discriminator
    of nkInt: intVal: int
    of nkFloat: floatVal: float
    of nkString: strVal: string
    of nkAdd, nkSub:
      leftOp, rightOp: Node
    of nkIf:
      condition, thenPart, elsePart: Node

var n = Node(kind: nkFloat, floatVal: 1.0)
# the following statement raises an `FieldDefect` exception, because
# n.kind's value does not fit:
n.strVal = ""

Como se puede ver en el ejemplo, una ventaja de una jerarquía de objetos es que no se necesita conversión entre diferentes tipos de objetos. Sin embargo, el acceso a campos inválidos de objeto generan una excepción.

Hay un azúcar sintáctico (Syntax Sugar) para llamar rutinas: Se puede utilizar la sintaxis obj.method(args) en lugar de method(obj, args).

Si no usa argumentos, se pueden omitir los paréntesis: obj.len en lugar de len(obj).

Esta sintaxis de llamada al método no está restringida a objetos, se puede utilizar para cualquier tipo:

import strutils

echo "abc".len # is the same as echo len("abc")
echo "abc".toUpperAscii()
echo({'a', 'b', 'c'}.card)
stdout.writeLine("Hallo") # the same as writeLine(stdout, "Hallo")

(Otra forma de ver la sintaxis de llamada a método es que proporciona la notación postfija)

Así que el código "puramente orientado a objetos" es fácil de escribir:

import strutils, sequtils

stdout.writeLine("Give a list of numbers (separated by spaces): ")
stdout.write(stdin.readLine.splitWhitespace.map(parseInt).max.`$`)
stdout.writeLine(" is the maximum!")

Como muestra el ejemplo anterior, Nim no necesita get-properties (Getters)

Procedimientos de obtención ordinarios (Getters) se llaman con la sintaxis de llamada a método logran lo mismo. Pero establecer un valor es diferente; para esto una sintaxis especial de Setter es necesario:

type
  Socket* = ref object of RootObj
    h: int # cannot be accessed from the outside of the module due to missing star

proc `host=`*(s: var Socket, value: int) {.inline.} =
  ## setter of host address
  s.h = value

proc host*(s: Socket): int {.inline.} =
  ## getter of host address
  s.h

var s: Socket
new s
s.host = 34  # same as `host=`(s, 34)

El ejemplo también muestra procedimientos "en-línea" (Inlined).

El operador de acceso [] se puede sobrecargar para proporcionar propiedades de la matriz:

type
  Vector* = object
    x, y, z: float

proc `[]=`* (v: var Vector, i: int, value: float) =
  # setter
  case i
  of 0: v.x = value
  of 1: v.y = value
  of 2: v.z = value
  else: assert(false)

proc `[]`* (v: Vector, i: int): float =
  # getter
  case i
  of 0: result = v.x
  of 1: result = v.y
  of 2: result = v.z
  else: assert(false)

El ejemplo es tonto, ya que un vector se modela mejor con una tupla que ya proporciona acceso v[].

Los procedimientos siempre utilizan el envío estático. Para el envío dinámico, reemplace la palabra clave proc por method:

type
  Expression = ref object of RootObj ## abstract base class for an expression
  Literal = ref object of Expression
    x: int
  PlusExpr = ref object of Expression
    a, b: Expression

# watch out: 'eval' relies on dynamic binding
method eval(e: Expression): int {.base.} =
  # override this base method
  quit "to override!"

method eval(e: Literal): int = e.x
method eval(e: PlusExpr): int = eval(e.a) + eval(e.b)

proc newLit(x: int): Literal = Literal(x: x)
proc newPlus(a, b: Expression): PlusExpr = PlusExpr(a: a, b: b)

echo eval(newPlus(newPlus(newLit(1), newLit(2)), newLit(4)))

Tenga en cuenta que en el ejemplo los constructores newLit y newPlus son proc porque tiene más sentido para ellos usar enlaces estáticos, pero eval es un método porque requiere un enlace dinámico.

Nota: A partir de Nim 0.20, para usar Multi-métodos, uno debe pasar explícitamente --multimethods:on al compilar.

En un método múltiple, todos los parámetros que tienen un tipo de objeto se utilizan para despacho:

type
  Thing = ref object of RootObj
  Unit = ref object of Thing
    x: int

method collide(a, b: Thing) {.inline.} =
  quit "to override!"

method collide(a: Thing, b: Unit) {.inline.} =
  echo "1"

method collide(a: Unit, b: Thing) {.inline.} =
  echo "2"

var a, b: Unit
new a
new b
collide(a, b) # output: 2

Como demuestra el ejemplo, la invocación de un método múltiple no puede ser ambigua: Se prefiere colisionar 2 a colisionar 1 porque la resolución funciona de izquierda a derecha. derecho. Por lo tanto, se prefiere Unit, Thing (Unidad, Cosa) a Thing, Unit (Cosa, Unidad).

Nota de rendimiento: Nim no produce una tabla de método virtual, pero genera árboles de despacho. Esto evita la costosa rama indirecta del método. y habilita la inserción. Sin embargo, otras optimizaciones como evaluacion en tiempo de compilación no funcionan con métodos.

En Nim, las excepciones son objetos. Por convención, los tipos de excepción son con el sufijo "Error". El módulo system define una jerarquía de excepciones. Las excepciones se derivan de system.Exception, que proporciona una interfaz común.

Las excepciones deben asignarse en el Heap porque se desconoce su duración. El compilador evitará la creacion de una excepción creada en el Stack. Todas las excepciones planteadas deberían al menos especificar la razón por la que se plantearon en el campo msg.

Una convención es que deben plantearse excepciones en casos excepcionales, no deben utilizarse como método alternativo de control de flujo.

La creación de una excepción se hace con la declaración raise:

var
  e: ref OSError
new(e)
e.msg = "the request to the OS failed"
raise e

Si la palabra clave raise no va seguida de una expresión, la última excepción es re-raised. Con el fin de evitar repetir este patrón de código común, el template newException en el módulo system se puede utilizar:

raise newException(OSError, "the request to the OS failed")

El try maneja excepciónes:

from strutils import parseInt

# read the first two lines of a text file that should contain numbers
# and tries to add them
var
  f: File
if open(f, "numbers.txt"):
  try:
    let a = readLine(f)
    let b = readLine(f)
    echo "sum: ", parseInt(a) + parseInt(b)
  except OverflowDefect:
    echo "overflow!"
  except ValueError:
    echo "could not convert string to integer"
  except IOError:
    echo "IO error!"
  except:
    echo "Unknown exception!"
    # reraise the unknown exception:
    raise
  finally:
    close(f)

Las declaraciones después del try se ejecutan a menos que se haga una excepción. Luego se ejecuta la parte apropiada except.

El except vacio se ejecuta si hay una excepción que no es enumerada explícitamente. Es similar a un else en un if.

Si hay una parte finally, siempre se ejecuta después de todo.

La excepción se consume en una parte except. Si una excepción no es manejada, se propaga a través de la pila de funciones (Call Stack). Esto significa que a menudo el resto del procedimiento que no está dentro de una cláusula de finally no se ejecuta (si ocurre una excepción).

Si necesita acceder al objeto o mensaje de la excepción real dentro de una rama except puede usar getCurrentException() y getCurrentExceptionMsg() de system. Ejemplo:

try:
  doSomethingHere()
except:
  let
    e = getCurrentException()
    msg = getCurrentExceptionMsg()
  echo "Got exception ", repr(e), " with message ", msg

Mediante el uso del pragma opcional {.raises.} puede especificar que un proc está destinado a generar un conjunto específico de excepciones, o ninguna en absoluto. Si se usa pragma {.raises.}, el compilador verificará que esto sea cierto. por Por ejemplo, si especifica que un proc genera IOError, y en algún momento comienza a generar una nueva excepción el compilador evitarra que ese proc se compile. Ejemplo de uso:

proc complexProc() {.raises: [IOError, ArithmeticDefect].} =
  ...

proc simpleProc() {.raises: [].} =
  ...

Una vez que tenga un código como este, si la lista de excepciones planteadas cambia el compilador se detendrá con un error al especificar la línea del proceso que dejó de validar el pragma y la excepción generada no se capturó, junto con el archivo y la línea donde se genera la excepción no detectada, que puede ayudarlo a localizar el código infractor que ha cambiado.

Si desea agregar el pragma {.raises.} a código existente, el compilador puede también ayudar. Puede agregar la declaración pragma {.effects.} a su proc y el compilador generará todos los efectos inferidos hasta ese punto, el seguimiento de excepciónes (Exception-Tracking) es parte del sistema de efectos (Side-Effects) de Nim. Otra forma más indirecta de averiguar la lista de excepciones planteadas por un proc es utilizar el comando de terminal doc de Nim que genera documentación para un módulo completo y decora todos proc con la lista de excepciones planteadas. Puedes leer más sobre el sistema de efectos y pragmas relacionados en el manual de Nim.

Los genéricos son los medios de Nim para parametrizar procesos, iteradores o tipos con tipos de parámetros. Los parámetros genéricos se escriben dentro de un corchetes cuadrados, por ejemplo, Foo[T]. Son útiles para tipos eficientes de contenedores Type-Safe.

type
  BinaryTree*[T] = ref object # BinaryTree is a generic type with
                              # generic param ``T``
    le, ri: BinaryTree[T]     # left and right subtrees; may be nil
    data: T                   # the data stored in a node

proc newNode*[T](data: T): BinaryTree[T] =
  # constructor for a node
  new(result)
  result.data = data

proc add*[T](root: var BinaryTree[T], n: BinaryTree[T]) =
  # insert a node into the tree
  if root == nil:
    root = n
  else:
    var it = root
    while it != nil:
      # compare the data items; uses the generic ``cmp`` proc
      # that works for any type that has a ``==`` and ``<`` operator
      var c = cmp(it.data, n.data)
      if c < 0:
        if it.le == nil:
          it.le = n
          return
        it = it.le
      else:
        if it.ri == nil:
          it.ri = n
          return
        it = it.ri

proc add*[T](root: var BinaryTree[T], data: T) =
  # convenience proc:
  add(root, newNode(data))

iterator preorder*[T](root: BinaryTree[T]): T =
  # Preorder traversal of a binary tree.
  # This uses an explicit stack (which is more efficient than
  # a recursive iterator factory).
  var stack: seq[BinaryTree[T]] = @[root]
  while stack.len > 0:
    var n = stack.pop()
    while n != nil:
      yield n.data
      add(stack, n.ri)  # push right subtree onto the stack
      n = n.le          # and follow the left pointer

var
  root: BinaryTree[string] # instantiate a BinaryTree with ``string``
add(root, newNode("hello")) # instantiates ``newNode`` and ``add``
add(root, "world")          # instantiates the second ``add`` proc
for str in preorder(root):
  stdout.writeLine(str)

El ejemplo muestra un árbol binario genérico. Dependiendo del contexto, los corchetes son utilizados para introducir parámetros de tipo o para instanciar un proc genérico. Como muestra el ejemplo, los genéricos funcionan con sobrecarga: Se utiliza la mejor coincidencia de add. El proc de add incorporado para secuencias no está oculto y se usa en el iterador preorder.

Hay una sintaxis especial "[: T]" cuando se utilizan genéricos con la sintaxis de llamada a método:

proc foo[T](i: T) =
  discard

var i: int

# i.foo[int]() # Error: expression 'foo(i)' has no type (or is ambiguous)

i.foo[:int]() # Success

Los templates son un mecanismo de sustitución simple que opera en árboles de sintaxis abstracta (AST) de Nim. Las plantillas se procesan en el paso semántico del compilador. Se integran bien con el resto del idioma y no comparten ninguna de las fallas de macros del preprocesador de C.

Para invocar un template, llámalo como un procedimiento.

Ejemplo:

template `!=` (a, b: untyped): untyped =
  # this definition exists in the System module
  not (a == b)

assert(5 != 6) # the compiler rewrites that to: assert(not (5 == 6))

Los operadores !=, >, >=, in, notin, isnot son templates:

Esto tiene el beneficio de que si sobrecarga el operador ==, el operador != está disponible automáticamente y hace lo correcto. (Excepto para números de coma flotante IEEE: NaN rompe la lógica booleana básica).

a > b se transforma en b < a. a in b se transforma en contains(b, a). notin y isnot tienen los significados obvios.

Los templates son especialmente útiles para fines de evaluación Lazy:

const
  debug = true

proc log(msg: string) {.inline.} =
  if debug: stdout.writeLine(msg)

var
  x = 4
log("x has the value: " & $x)

Este código tiene una deficiencia: si debug es falso algún día, ¡Las costosas operaciones $ y & todavía se realizan!

Convertir el proceso log en un template resuelve este problema:

const
  debug = true

template log(msg: string) =
  if debug: stdout.writeLine(msg)

var
  x = 4
log("x has the value: " & $x)

Los tipos de parámetros pueden ser tipos ordinarios o metatipos untyped, typed o type. type sugiere que solo se puede dar un símbolo de tipo como argumento, y untyped significa que las búsquedas de símbolos y la resolución de tipos no son realizado antes de que la expresión se pase al template.

Si el template no tiene un tipo de retorno explícito, void se utiliza para mantener la coherencia con los procesos y métodos.

Para pasar un bloque de codigo como argumento a un template, use untyped para el último parámetro:

template withFile(f: untyped, filename: string, mode: FileMode,
                  body: untyped) =
  let fn = filename
  var f: File
  if open(f, fn, mode):
    try:
      body
    finally:
      close(f)
  else:
    quit("cannot open: " & fn)

withFile(txt, "ttempl3.txt", fmWrite):
  txt.writeLine("line 1")
  txt.writeLine("line 2")

En el ejemplo, las dos declaraciones writeLine están vinculadas al parametro body. El template withFile contiene código repetitivo y ayuda a evitar un error común: olvidar cerrar el archivo. Note como la declaración let fn = filename garantiza que solo se evalúe filename una vez.

import math

template liftScalarProc(fname) =
  ## Lift a proc taking one scalar parameter and returning a
  ## scalar value (eg ``proc sssss[T](x: T): float``),
  ## to provide templated procs that can handle a single
  ## parameter of seq[T] or nested seq[seq[]] or the same type
  ##
  ## .. code-block:: Nim
  ##  liftScalarProc(abs)
  ##  # now abs(@[@[1,-2], @[-2,-3]]) == @[@[1,2], @[2,3]]
  proc fname[T](x: openarray[T]): auto =
    var temp: T
    type outType = typeof(fname(temp))
    result = newSeq[outType](x.len)
    for i in 0..<x.len:
      result[i] = fname(x[i])

liftScalarProc(sqrt)   # make sqrt() work for sequences
echo sqrt(@[4.0, 16.0, 25.0, 36.0])   # => @[2.0, 4.0, 5.0, 6.0]

El código Nim se puede compilar a JavaScript. Sin embargo, para escribir código compatible con JavaScript, debe recordar lo siguiente:

Clone this wiki locally