DNM+ Online
dotnetmania 2.0
Representación funcional de matrices numéricas
Este artículo introduce un tipo de datos para representar matrices numéricas programado en F# que utiliza funciones en lugar de arrays numéricos. Se describe cómo esta nueva abstracción puede contribuir a reducir el consumo de memoria para representar matrices, al tiempo que confiere a la evaluación de operadores un carácter diferido y admite un estilo de codificación similar a la notación matemática. Más adelante se discute cómo estas características y la utilización del patrón iterador permiten expresar computaciones que sirvan de entrada a Parallel LINQ. Por último, se presenta una personalización de la herramienta F# Interactive que ofrece soporte para el tipo de datos desarrollado a lo largo del texto.

La representación de matrices numéricas mediante arrays bidimensionales es una práctica muy extendida dada la relación simbólica que existe entre el concepto del álgebra lineal y la semántica que estas construcciones ofrecen. En el caso de las denominadas matrices esparcidas (sparse matrices), existen además, muchas convenciones que sacan partido del número elevado de valores nulos para reducir el costo del almacenamiento en memoria y mejorar los tiempos de respuesta; aunque para una matriz esparcida de dimensiones suficientemente grandes la huella en memoria aún puede resultar excesiva. Este artículo sugiere una nueva abstracción que permite el trabajo con matrices sin calcularlas totalmente, sino calculando solo aquellos elementos que se necesiten en un momento dado, utilizando para tal fin una función que permite obtener un elemento a partir de sus coordenadas. Para la implementación se utilizó el lenguaje F#, cuya naturaleza funcional y orientada a objetos permite encapsular rutinas y algoritmos funcionales en tipos de datos compatibles con CLI. Una vez presentado el tipo de datos, se analizará una consecuencia inmediata del uso de una función para identificar a una matriz, que consiste en la posibilidad de definir iteradores que recorran sus elementos y expresen de manera declarativa computaciones que sirvan de entrada a la tecnología PLINQ. Por último, presentamos una personalización de la herramienta F# Interactive para experimentar con el nuevo tipo de datos. Matrices numéricas y arrays El empleo de arrays bidimensionales para programar un tipo de datos que represente matrices numéricas (en lo sucesivo se empleará únicamente el término matriz) es una práctica muy extendida. Es ciertamente difícil encontrar otra estructura de datos que guarde una relación tan estrecha con el concepto del álgebra lineal que este recurso, tan familiar para los programadores y disponible en los lenguajes de programación más utilizados. Pero esta relación no es perfecta en la práctica, y el empleo de arrays, aunque natural, añade a la resolución de problemas numéricos vía matrices consideraciones importantes acerca del estilo de codificación empleado en la definición de operaciones, así como de cuánta memoria y tiempo requieren éstas. El carácter finito de la memoria de cualquier máquina computadora garantiza la existencia de una cota superior para un determinado vector de dimensiones suficientemente grande. Si bien esta afirmación puede resultar extrema, la complejidad inherente del mundo en que vivimos con facilidad provee problemas que involucran un número finito, pero grande, de variables que requieren ser optimizadas mediante el empleo de sistemas lineales [1,2]. Una clase especial de matriz, denominada esparcida, tiene la particularidad de contar con un número elevado de elementos nulos, característica aprovechada en muchas convenciones existentes [3] para almacenar solo aquellos elementos diferentes del que predomina. En esta familia de abstracciones, el uso de arrays tiene una presencia más discreta, y la cota superior es mucho mayor que en el caso de un array bidimensional. Por supuesto, estos beneficios se obtienen si y solo si se está en presencia de una auténtica matriz esparcida. Por otra parte, las operaciones entre matrices en representaciones que utilizan arrays deben agotar todas las coordenadas de la matriz resultante calculando los elementos asociados, antes de realizar cualquier otra acción. Dicho proceso aumenta el tiempo de procesamiento y en casos donde solo una porción de los datos es relevante puede llegar a ser fútil. Además, esta codificación, al ser más explícita, difiere de la notación matemática, mucho más declarativa. ¿Será posible aplicar prácticas propias de la programación funcional tales como la evaluación diferida y código declarativo al trabajo de matrices? ¿Podría disponerse de una abstracción de matrices que no utilice arrays?

Abstracción funcional El empleo de las convenciones para matrices esparcidas mencionadas en la sección anterior está justificado por la existencia de un isomorfismo entre el espacio vectorial de estas estructuras y el espacio vectorial de las matrices. Este concepto del álgebra lineal establece una relación uno a uno entre dos estructuras basada en una función (o aplicación) biyectiva que traslada un elemento de una de ellas en su equivalente en la otra. Entonces es válido economizar recursos utilizando representaciones esparcidas para matrices que cumplan esta característica, ya que la existencia del isomorfismo garantiza un enlace entre unas coordenadas válidas y su elemento correspondiente cualquiera sea la estructura.
Manteniendo dicho enlace sin importar qué recursos se utilicen define implícitamente una relación isomorfa, y se está en presencia de una abstracción para representar matrices. Nótese entonces que cualquier abstracción de una matriz queda determinada por la información de las dimensiones y la función que permite inspeccionar los elementos. Ahora bien, el conjunto de funciones numéricas, en este caso de dos variables pertenecientes a los números naturales, es un conjunto vasto y en extremo versátil. Reflexione el lector si tomando el conjunto de las coordenadas válidas asociadas a las dimensiones de una matriz será posible encontrar una función que al ser evaluada en dichas coordenadas dé como resultado los elementos correspondientes. La respuesta es afirmativa, y es importante destacar que dicha función no tiene que ser un reflejo exacto de la notación matemática.
Como ejemplo, tómese el conjunto de las matrices cuadradas cuyos elementos son los números naturales ordenados de manera ordinaria. Fijando el orden como 3, se tiene la siguiente matriz: La construcción más ingenua se basa en el uso de condicionales atendiendo al índice de fila y columna, como se muestra en el listado 1a. Sencillo pero efectivo, y nada despreciable si se tiene en cuenta que no todos los valores numéricos involucrados se cargan en la memoria. No obstante, otras funciones más eficientes pueden construirse, dado que esta matriz y el conjunto al que pertenece tienen un patrón bien definido, que permite computar el valor de un elemento a partir de sus coordenadas. Las funciones de los listados 1b y 1c sacan partido de este hecho, y en particular la función simpleC se vale de las dimensiones (identificadores rowCount y columnCount), permitiendo escalar esta clase de matriz a grandes dimensiones utilizando la misma función.

Sin duda, se trata de un criterio más económico que mantener todos los números naturales en un array bidimensional. Al igual que este ejemplo, hay otras matrices cuyos valores pueden calcularse a partir de sus coordenadas, como es el caso de las matrices Identidad, Nula, Hankel, Hilbert o Vandermonde, entre otras. La representación trivial, no obstante, es una prueba de existencia de esta función que llamaremos generadora y que justifica la implementación en las próximas secciones de una abstracción puramente funcional, donde una matriz se caracteriza por una tupla de la forma:

(Número de Filas, Número de
Columnas, Función Generadora)

El tipo de datos FMatrix Tal y como se comentó al inicio del texto, el lenguaje F# [4] destaca por introducir el paradigma funcional para la programación sobre la arquitectura de CLI; al igual que ocurre con otros lenguajes .NET, con F# es posible declarar tipos de datos que pueden ser consumidos desde cualquier otro lenguaje de la plataforma, como C# y Visual Basic. Un punto de convergencia de las construcciones funcionales y el sistema de inferencia de F# es la característica denominada construcción implícita de tipos, mediante la cual se utiliza una tupla como la presentada al final de la sección anterior para especificar de manera sucinta los campos que componen un tipo de datos y dejar que el compilador se encargue de generar los constructores apropiados. En el listado 2 se comienza la implementación del tipo de datos FMatrix utilizando este mecanismo. Nótese como no se especifica el tipo de datos de cada uno de los componentes de la tupla (rows, columns, gen); será el compilador de F# el que los detectará a partir de su utilización en el código.
La primera instrucción del cuerpo de la clase comienza con la palabra reservada do, que permite añadir al constructor principal la invocación a una acción que no devuelve ningún valor significativo. Aquí se ejecuta la función auxiliar requires para establecer una precondición que prevenga la construcción de matrices con dimensiones negativas. El motivo de por qué se permite la creación de matrices con un número de filas y de columnas nulas se trata en la próxima sección. La siguiente instrucción declara un segundo constructor cuyo tercer parámetro es del tipo delegado Func<Int32,Int32, Double>. Dicha sobrecarga es necesaria dado que el identificador gen y, en consecuencia, el tercer parámetro del primer constructor se infieren como de tipo FSharpFunc<Tuple<Int32,Int32>,Double>.Un hecho que previene que desde otros lenguajes puedan crearse nuevas instancias de la clase utilizando expresiones lambda:

//C# new FMatrix(128, 128, (i,j) =>
Math.Pow(-1, i + j));

'Visual Basic New FMatrix(128, 128, Function(i,j)
Math.Pow(-1, i + j)) Esta incompatibilidad es debida a la manera en que F# soporta funciones que son tratadas como datos1, para la que no emplea delegados, sino subclases de FSharpFunc<T, TResult> generadas por el compilador. La solución, como puede observarse, es sencilla, y consiste en llamar al método Invoke del delegado desde el cuerpo de una expresión lambda que a continuación será utilizada como argumento para la llamada al primer constructor. Las restantes instrucciones declaran un conjunto de propiedades de solo lectura para brindar información sobre la instancia. Las propiedades RowCount y ColumnCount son necesarias, pues los identificadores involucrados en la construcción implícita tienen visibilidad privada. Los identificadores de miembros (palabra reservada members) de la clase asociados a una instancia deben estar prefijados por un identificador y el carácter punto; este identificador estará asociado a la instancia de la clase y no se requiere que sea el mismo para todos los miembros de instancia, aunque en el código del listado se utiliza this por afinidad con C#. Matrices vacías Antes de comenzar en las próximas secciones la implementación de métodos y operadores, es necesario introducir la idea de una matriz que no contenga elementos, es decir una matriz vacía. Aunque no es un concepto que pueda visualizarse, su definición: una matriz en la que al menos una de sus dimensiones es igual a cero, se prueba consistente con la teoría de matrices que se introduce en los textos de Algebra Lineal [5].
Al igual que su relevancia teórica radica en que resulta conveniente para comenzar formulaciones y demostraciones inductivas, para la perspectiva de un programador constituye un recurso natural para servir de base a procedimientos recursivos. Un ejemplo es el cálculo recursivo de un determinante por la vía de los menores, donde el caso base es el determinante de una matriz vacía de 0x0, el cual debe ser aceptado como 1.
A continuación se reproduce la propuesta de [6] para incorporar matrices vacías al dominio de operaciones básicas tales como la multiplicación de una matriz por un escalar (1), suma de matrices (2), multiplicación de matrices (3-5) así como otras relaciones (6-7).

Préstese atención a que el objetivo de esta formalización no es probar suerte con ciertas proposiciones, sino permitir el trabajo con matrices vacías respetando los requerimientos usuales de cada operación. Por ejemplo, no tiene sentido sumar una matriz de 5x5 con una matriz de 0x0 dado que no tienen iguales dimensiones; si se obvia este hecho y se establece que dicha operación dé como resultado la matriz de 5x5, se introduce una laguna en la implementación del contrato de la operación de suma, que puede derivar en una sobre-verificación de casos en futuros procedimientos.

Operaciones elementales A continuación se discute la implementación de operaciones elementales en el tipo de datos FMatrix y se comentan sus implicaciones para trabajar con matrices de manera funcional. La idea detrás de cada procedimiento es crear una nueva función generadora que se valga de las funciones generadoras anteriores.

  1. Obtención y cambio de un elemento a partir de sus coordenadas Para obtener un elemento a partir de sus coordenadas, F# ofrece soporte para declarar indexadores mediante la declaración de una propiedad con el nombre Item, como puede verse en el listado 3. Repárese en que los elementos de una matriz solo serán calculados cuando sean necesarios, y cuando esto sucede, no son almacenados por el código del tipo de datos, previniendo el sobreconsumo de memoria. Para sustituir el valor de un elemento, debe recordarse que una abstracción funcional demanda que el tipo de datos sea inmutable y cualquier transformación dé como resultado una nueva instancia en lugar de la modificación de una existente.
    Por este motivo, no es posible utilizar la sección set de una propiedad, pues el resultado de su invocación debe ser el valor no significativo unit; en su lugar se declara un método ChangeItem que recibe una tupla de la forma (fila, columna, nuevo valor). El procedimiento para sustituir el elemento consiste en crear una función con una condicional que ante coordenadas diferentes delega en el indexador de la instancia en cuestión, es decir en la función generadora original.

  2. Permutación de filas y columnas En el listado 4, la nueva función generadora utiliza una condicional para invertir una de las coordenadas. Repare el lector en cómo los parámetros r1 (c1) y r2 (c2) no pertenecen al contexto de ejecución de la nueva función y sin embargo son empleados en su definición. En presencia de esta situación, se dice que los parámetros son capturados por la clausura (closure) de la expresión lambda.

  3. Obtención de la transpuesta La función generadora de la matriz transpuesta disponible en el listado 5 destaca por su brevedad y sobre todo por su semejanza con la notación matemática.

    4.  Concatenación horizontal y vertical En el código del listado 6 puede verse cómo la concatenación de matrices tanto vertical como horizontal impone condiciones sobre las columnas y filas, respectivamente. Una vez validada la operación, la matriz resultante de la operación tendrá una dimensión mayor, y la función generadora dependerá de las funciones generadoras involucradas. El uso de los métodos ConcatHorizontal y ConcatVertical puede resultar muy útil en el caso que se desee componer una matriz a partir de bloques cuya funciones generadoras tienen una expresión conocida, no así el resultado de su concatenación.

  4. Suma de matrices Al igual que en el caso de la función generadora de la matriz transpuesta, la función correspondiente a la suma es semejante a la notación que puede encontrarse en los textos de álgebra lineal. Como la primera instrucción del método incluido en el listado 7 utiliza una propiedad del tipo FMatrix, es necesario declarar explícitamente el tipo de datos, pues en esa posición no hay información que permita al motor de inferencia restringir a un único tipo. Aunque la sintaxis para sobrecargar un operador es similar a C# y Visual Basic, el compilador de F# permite definir operadores simbólicos mucho más arbitrarios, con el inconveniente que solo será posible utilizarlos como operadores desde el propio F#.

  5. Multiplicación de una matriz por un escalar Similar al anterior, el trabajo con funciones generadoras permite definir operaciones de manera declarativa en el listado 8. Esta vez, en lugar de una propiedad se utiliza un indexador que requiere que el tipo de datos sea declarado; al detectar que este indexador tiene como resultado un valor del tipo System.Double, el compilador de F# resuelve que éste es el tipo de datos del parámetro scalar.

  6. Multiplicación de matrices Sin dudas, para esta operación la representación tiene como principal atractivo el que solo se calcularán aquellos elementos que sean necesarios, reduciendo o igualando (en dependencia del programa) el costo de orden cúbico que obtenemos al codificar utilizando arrays. También vuelve a obtenerse un código conciso, pues en el listado 9, una vez resueltos los casos asociados a las matrices vacías, la nueva función generadora es una traducción del símbolo sumatoria correspondiente al elemento que se encuentra en la coordenada (i, j): La instrucción seq {1 .. b.RowCount} es una expresión de rango (range expression) que representa una sucesión de números naturales que enumera los índices de las filas de la matriz situada a la derecha. El operador de consolidación fold procesa estos números y calcula el total mediante una función anónima que se corresponde con el cuerpo de la sumatoria anterior. Como los elementos de la matriz son de tipo System.Double, el segundo parámetro del método fold debe ser el literal 0.0.
    Una alternativa a la fórmula tradicional lo constituye el algoritmo de multiplicación de Strassen [7]. El lector interesado puede consultar su implementación para el nuevo tipo de datos en el código fuente que acompaña el artículo.
  7. Extracción de submatrices Aunque la definición de submatriz surge del propio concepto de matriz, son muy pocos los lenguajes de programación que incorporan en sus primitivas una sintaxis para expresar esta idea de manera sencilla. Este no es el caso en F#, y tal como ocurre con los indexadores, el compilador soporta una convención para transformar una sencilla sintaxis a la llamada de un método específico. En la tabla 1 pueden verse algunos ejemplos de dicha convención, definiendo una porción rectangular a través de cuatro valores opcionales. La convención exige una propiedad identificada por GetSlice tal que sus parámetros estén dentro de los valores admisibles de la unión discriminada Option, un tipo de datos que permite expresar un valor opcional, y que en este caso será de tipo System.Int32. La instrucción matrix.[3 .. , 10 .. 30] será resuelta por el compilador en:

    matrix.GetSlice(Some(3), None, Some(10), Some(30))

    Donde la ausencia del tercer valor indica que debe extraerse una submatriz que comprenda los elementos en la intersección de todas las filas salvo las dos primeras, y las columnas desde la décima hasta la trigésima. Al ser Option una unión discriminada, el compilador permite utilizar los identificadores que agrupa para filtrar una instancia y extraer la información útil. En el código del listado 10, la función adaptSlice incorpora una construcción match … with para en cuatro líneas de código cubrir todos los casos posibles, cuidando de que si un parámetro representa un valor, el mismo esté asociado a un identificador en la rutina encargada de procesar esa variante. Para procesar la dicotomía de los valores opcionales, se ha utilizado una de las características más atractivas de F# llamada emparejamiento de patrones (pattern matching), consistente en aplicar patrones simbólicos y asociativos a valores e identificadores. Analizar en detalle todo el potencial de esta característica se escapa de las motivaciones de este artículo, pero se recomienda al lector que compare el código del listado con un equivalente en C# o Visual Basic. Tras comprobar que los índices son válidos, la función generadora delega en el indexador de la instancia en contexto, capturada por la clausura de la expresión lambda.

  8. Construcción de la función
    generadora Como se hizo notar al principio de este artículo, dada cualquier matriz es posible construir una función que la genere utilizando un esquema basado en condicionales. Pero como es lógico, esta conclusión no es particularmente útil a menos que se implemente un procedimiento para reproducir dicho esquema cualquiera sea la matriz. Además, en consonancia con el resto de los códigos presentados, es deseable que el procedimiento que fabrique la función generadora exhiba únicamente código funcional. Para introducir un algoritmo que reproduzca el esquema condicional y su implementación, analicemos el código del listado 11, el cual define una función generadora que reproduce una matriz cuadrada con los cuatro primeros números naturales.
    La función asociada al identificador simpleA puede parecer similar a su homónima en el primer ejemplo de este artículo, con la diferencia que representa una matriz de orden 2 y las instrucciones elif han sido sustituidas por la definición de funciones anónimas que son inmediatamente evaluadas con los parámetros de la función que la invoca. Para hacer más legible el código, en cada declaración de una función anónima se ha añadido al identificador una numeración para indicar la coordenada correspondiente en la matriz; aunque al tratarse de funciones anónimas perfectamente delimitadas, el código compilaría sin esta diferenciación. Nótese que cada función anónima, salvo aquellas que emiten un mensaje de error2, invocan en su cuerpo a la función que representa a la próxima coordenada, ya sea de fila o columna. Extrayendo cada función anónima como un parámetro que debe ser suministrado, la función anónima correspondiente a la primera fila y primer valor de la matriz pudieran escribirse de la siguiente manera:

    let row1 value11Gen nextRow  =
    fun (i,j) -> if i = 1 then value11Gen(j)
    else nextRow(i,j)

    let value11 nextColumn =
    fun j -> if j = 1 then 1.0
    else nextColumn(j) Repitiendo este proceso y notación para cada una de las funciones anónimas, es posible reescribir la función simpleA como:

    let simpleA =
    (row1 (value11(value12(endOfColumn)))
    (row2 (value21(value22(endOfColumn)))
    endOfRows))

    La observación de importancia en este ejemplo es que la función generadora deseada puede obtenerse construyendo pequeñas funciones cuyo único parámetro sea de tipo función y su resultado en cambio sea una función que acepte una o dos coordenadas; nótese que la firma de la función que debe ser pasada como parámetro coincide con el de la función resultante de suministrarlo. Claro que deben delimitarse dos familias de esta clase de funciones: las que recorren las filas y las que recorren los valores de éstas. En el listado 12 puede verse esta idea puesta en práctica. La función createGen tiene como primer parámetro un valor opcional que indica si el esquema condicional debe optimizarse para reflejar la repetición de un valor en la matriz3, mientras que el segundo parámetro se espera sea una secuencia de secuencias de números reales que caracterice a la matriz, desarrollándola por filas. En las primeras instrucciones se declaran las funciones que servirán de valor inicial y final para la generación de las funciones que representaran a las filas y las columnas. Repárese en que de tratarse de una matriz esparcida, la última función que será aplicada en la representación de una fila no lanza un mensaje de error, sino que devolverá el valor cero. La idea que se dedujo de analizar la función simpleA debe quedar más clara viendo la implementación de las funciones buildMatrixGen y buildRowGen, las cuales utilizan el operador foldic4 y las funciones matrixFold y rowFold, respectivamente, para fabricar la función generadora final. En ambos casos, el resultado principal de la consolidación es una función que toma un único parámetro de tipo función, por lo que en cada iteración debe definirse otra función anónima que tenga una función como primer parámetro. De esta manera, una vez terminada la consolidación solo es necesario aplicar al resultado las funciones identificadas por lastMatrixGen y lastRowGen, lo que por fin dará como resultado funciones que aceptan coordenadas. Fíjese el lector en que además de componer estas pequeñas funciones, el uso del operador foldic permite verificar que no se procesen filas con cantidad de elementos diferentes, así como, una vez construida la función generadora, las dimensiones de la matriz estén perfectamente determinadas sin necesitar que llamar a un operador como count. La función createGen, a diferencia del resto de las operaciones elementales que han sido presentadas, no está contenida en el tipo de datos FMatrix, sino en un módulo del mismo nombre. Un módulo de F# guarda cierta semejanza con su homónimo en Visual Basic: ambos permiten agrupar funcionalidad que no está asociada a una instancia de un tipo de datos, y al ser compilados tanto los valores como las funciones declaradas se convierten en miembros estáticos de una clase estática cuyo nombre coincide con el que recibe el módulo. Precisamente por este motivo se marca el módulo con el atributo CompilationRepresentation, especificando que al ser compilado se le añadirá el sufijo Module, aunque dicha anotación será perceptible únicamente desde otros lenguajes diferentes de F#. Las restantes funciones del listado, fromSeq y fromSeqSparse, a diferencia de createGen, no son privadas y constituyen, junto con otras funciones que dependen de createGen, la interfaz pública para crear matrices a partir de un texto o un desarrollo por filas. El patrón iterador y Parallel LINQ Hasta el momento, se ha garantizado un tipo de datos inmutable sobre el cual las operaciones toman un orden constante, aunque no así la obtención de los resultados. Para continuar la aplicación de prácticas funcionales en el tipo FMatrix, utilizaremos en este punto el patrón iterador. Las clases que exponen este patrón son reconocibles por implementar la interfaz IEnumerable<T>, y en F# se les conoce como secuencias. El mecanismo de iteración que ofrece este patrón es también de carácter perezoso y garantiza que en memoria existe solo un elemento de la secuencia sobre la que se itera. En el caso, del tipo FMatrix, este mecanismo se convierte en una herramienta muy útil al garantizar que, sin importar lo extenso y complejo de un recorrido sobre los elementos de una matriz, su iteración no demandará más memoria y tiempo de procesamiento que el requerido para calcular el elemento más exigente. En el listado 13 puede verse la implementación de propiedades como Items, ItemsIndexed y Rows que utilizan las expresiones de secuencia (sequence expressions) para construir iteradores y tienen como resultados objetos de tipo IEnumerable<Double>, IEnumerable<Int32, Int32,Double>, IEnumerable<IEnumerable<Double>>, respectivamente.
    Por otro lado, el método GetItemsBy recibe como entrada una secuencia de coordenadas y produce una secuencia con los elementos correspondientes. Un beneficio de utilizar el patrón iterador es la posibilidad de aplicar la tecnología LINQ para expresar computaciones que involucren a los elementos de una matriz. De hecho, al tratarse de un tipo de datos inmutable resulta menos complejo utilizar la implementación de los operadores de consulta propia de Parallel LINQ [8]. Nótese como al tratarse de matrices numéricas de números reales, donde las operaciones de suma algebraica y producto cumplen la propiedad asociativa, es menos problemático utilizar computaciones que involucren operadores de consolidación como Aggregate y Sum en su implementación paralela. Por ejemplo, para calcular la traza de una matriz desde C# utilizando PLINQ puede utilizarse el siguiente código:

    var coordinates =
    from x in Enumerable.Range(1,
    Math.Min(m.RowCount, m.ColumnCount)) select new Tuple<int, int>(x, x); m.GetItemsBy(coordinates).AsParallel().Sum();

    El cual al ejecutarse mantendrá un consumo memoria casi constante. A diferencia de C# y Visual Basic, la gramática de F# no incluye palabras claves que soporten los operadores de consulta estándar de LINQ; en su lugar, F# cuenta con el módulo Seq, mediante el cual pueden obtenerse resultados equivalentes a partir de funciones más a tono con las características del lenguaje. De manera similar, se dispone del módulo PSeq, que se apoya en Parallel LINQ.
    En el listado 14 puede encontrarse la implementación del método Equals, encargado de determinar cuando un objeto y la instancia en cuestión representan matrices idénticas. En el código, una vez se verifica que el parámetro es de tipo FMatrix y tiene las mismas dimensiones que la instancia, se procede a verificar si se trata de una comparación entre matrices vacías, en cuyo caso el resultado será verdadero. De no tratarse de matrices vacías, se definirá una secuencia de valores booleanos que contendrá el resultado de comparar elementos homólogos entre las matrices. A continuación, esta secuencia sirve de entrada al operador forall, que la distribuirá en las unidades de proceso disponibles verificando que al aplicar cada uno de sus elementos a la función ((=) true) el resultado sea verdadero. 
    Merthin Interactive
    La distribución del lenguaje F# incluye, además del compilador, una aplicación de consola llamada F# Interactive de tipo REPL (Read-Eval-Print Loop), que tras aceptar el código F# procede a compilarlo, ejecutarlo e imprimir los resultados de vuelta a la consola. Esta clase de programa resulta muy socorrido para evaluar pequeñas porciones de código, explorar clases y realizar cálculos sin necesidad de crear un proyecto en el entorno de desarrollo. Adicionalmente, esta herramienta permite indicar un script de F# (extensión .fsx) para ser ejecutado antes que el usuario comience a escribir a la consola, al tiempo que expone una pequeña interfaz para configurar la aplicación. Estas opciones permiten personalizar el entorno de F# Interactive para adecuarla a un dominio especifico; con el proyecto Merthin Interactive [9], hemos implementado un entorno interactivo de pruebas y cálculos al estilo de MATLAB basado en el tipo de datos FMatrix que le invitamos sinceramente a probar. La figura 1 muestra a Merthin Interactive en ejecución. Conclusiones En este artículo se ha introducido un nuevo tipo de datos para el trabajo con matrices numéricas, que aplicando un acercamiento enteramente funcional logra en cierta medida mantener un consumo de memoria constante y tiempos de respuesta ajustados a los resultados que se desean obtener, así como acercar la codificación de operaciones matriciales a la notación matemática. La abstracción presentada no es una propuesta de sustitución de otras existentes, sino un complemento para resolver problemas matriciales desde otro punto de vista. Una revisión obligada de este trabajo que podría ser motivo de futuros artículos sobre F# es la utilización de los árboles de expresiones de .NET 4.0 para representar tanto la función generadora como los elementos de la matriz. Lo primero tiene obvias aplicaciones como la optimización, mientras que lo segundo es indispensable para programar métodos del algebra lineal donde intervengan matrices cuyos coeficientes sean polinomios.
    Otra insuficiencia a superar que probablemente el lector habrá notado es la no utilización de la genericidad para permitir que las funciones generadoras devuelvan otros tipos de datos numéricos. La razón de esta ausencia es producto de que no es posible forzar un parámetro de tipo para que contenga operadores, y la solución correcta, que consiste en representar estructuras algebraicas como campos y anillos mediante tipos genéricos, implicaría un texto más extenso. La motivación detrás del código presentado en este artículo fue la de experimentar con la primera versión definitiva de F# desde los mandos de Visual Studio 2010. Espero que tras haber leído este texto el lector se sienta animado a probar la alternativa que supone este lenguaje y comprobar cuán adecuado resulta para resolver más de un problema complejo de una manera sencilla y elegante. La solución que incluye los códigos del artículo puede descargarse desde el sitio del proyecto Merthin en CodePlex [9].

blog comments powered by Disqus
autor
referencias