domingo, 29 de agosto de 2021

El bucle for in do.

La sentencia for in do (for in do loop) está disponible desde la versión 2.4.2 del compilador Free Pascal, es decir, es relativamente nueva y no existe ni en Pascal ni en Turbo Pascal, sí en Object Pascal de Delphi, aunque hay algunas "cosillas" que se pueden hacer con Free Pascal y no con Delphi, respecto de esta sentencia de iteración.

Seguramente para los nuevos (y no tan nuevos) programadores no hay ninguna novedad con este bucle, pero los que nos iniciamos mucho antes, dejamos la profesión unos años y retomamos en estos tiempos, hay cosas que cuestan un poco, no tenemos el concepto, nacimos con el for to do, while do, repeat until y de ser necesario, punteros, y con eso se hacía (y hace) de todo.

Las cosas cambian, los lenguajes evolucionan o mueren, aunque no siempre incorporar nuevas sentencias o lo que sea, signifique evolución, pero es preferible eso al estancamiento. Y la verdad que tanto Free Pascal y Delphi (ambos Object Pascal) conservan la belleza del lenguaje, esa sintaxis única.

Creo que lo mejor es empezar por responder ¿qué lo diferencia del for to do?:

En el for to se usa un índice, generalmente "i", entero, para recorrer los elemento de un vector (array) y acceder a ellos utilizando el valor de i, es decir obtenemos un valor de unarray[i]. En cambio en el for in para hacer lo mismo, usando una variable i y un array con índices enteros, la variable i va tomando los valores del array, los valores se obtienen de i. no de unarray[i].

Otra diferencia es que mediante el empleo de un for to se puede recorrer solo una parte, por ejemplo un array de 500 elementos con un for i:=1 to 10 do... solo accedemos a los 10 primeros, en cambio con el uso de for i in ... se recorre todo.

La variable de control debe ser del mismo tipo que los elementos del conjunto a recorrer.

El conjunto a iterar debe ser de un número fijo de elementos.

La variable es una copia temporal del elemento del bucle.

Ejemplos:

program pruebaforin;
{$mode objfpc}{$H+}
uses Classes;

type
  TSemana=(domingo, lunes, martes, miercoles, jueves, viernes, sabado);


var
  entero:Integer;
  arrEntero:array[1..12] of Integer=(1,2,4,8,5,6,7,8,9,27,11,12);
  cadena:String;
  arrCadena:array[1..5] of String =('Lunes','Martes','Miércoles','Jueves','Viernes');
  dia:TSemana;
  diaslaborables:set of TSemana =[lunes, martes, miercoles, jueves, viernes];
  caracter:Char;
  meses:TStringList;

begin
  WriteLn('Vector de eneteros');
  WriteLn('entero:Integer;');
  WriteLn('arrarrEntero:array[1..12] of Integer=(1,2,4,8,5,6,7,8,9,27,11,12);');
  WriteLn('for entero in arrEntero do WriteLn(''entero =  '',entero,''  arrEneter['',entero,'']='',arrEntero[entero]);');
  for entero in arrEntero do WriteLn('entero =  ',entero,'  arrEneter[',entero,']=',arrEntero[entero]);
  ReadLn;
  WriteLn('Vector de cadena de caracteres.');
  WriteLn('cadena:String;');
  WriteLn('arrCadena:array[1..5] of String =(''Lunes'',''Martes'',''Miércoles'',''Jueves'',''Viernes'');');
  WriteLn('for cadena in arrCadena do WriteLn(''Cadena = '',cadena);');
  for cadena in arrCadena do WriteLn('Cadena = ',cadena);
  ReadLn;
  WriteLn('Enumerados.');
  WriteLn('TSemana=(domingo, lunes, martes, miercoles, jueves, viernes, sabado); ');
  WriteLn('dia:TSemana;');
  WriteLn('for dia in Tsemana do WriteLn(''dia = '',dia);');
  for dia in Tsemana do WriteLn('dia = ',dia);
  ReadLn;
  WriteLn('Conjuntos.');
  WriteLn('TSemana=(domingo, lunes, martes, miercoles, jueves, viernes, sabado); ');
  WriteLn('diaslaborables:set of TSemana =[lunes, martes, miercoles, jueves, viernes];');
  WriteLn('dia:TSemana;');
  WriteLn('for dia in diaslaborables do WriteLn(''dia = '',dia);');
  for dia in diaslaborables do WriteLn('dia = ',dia);
  ReadLn;
  WriteLn('Caracteres.');
  WriteLn('caracter:Char;');
  WriteLn('for caracter in ''abcdefg'' do WriteLn(''caracter = '',caracter);');
  for caracter in 'abcdefg' do WriteLn('caracter = ',caracter);
  ReadLn;
  WriteLn('Classes.');
  WriteLn('cadena:String;');
  WriteLn('meses:TStringList;');
  WriteLn('for cadena in meses do WriteLn(''cadena = '',cadena);');
  meses:=TStringList.Create;
  meses.Add('Enero');
  meses.Add('Febrero');
  meses.Add('Marzo');
  meses.Add('Abril');
  meses.Add('Mayo');
  for cadena in meses do WriteLn('cadena = ',cadena);
  meses.Free;
  ReadLn;
end.      

Resultado del programa (salida por consola):

Vector de eneteros
entero:Integer;
arrarrEntero:array[1..12] of Integer=(1,2,4,8,5,6,7,8,9,27,11,12);
for entero in arrEntero do WriteLn('entero =  ',entero,'  arrEneter[',entero,']=',arrEntero[entero]);
entero =  1  arrEneter[1]=1
entero =  2  arrEneter[2]=2
entero =  4  arrEneter[4]=8
entero =  8  arrEneter[8]=8
entero =  5  arrEneter[5]=5
entero =  6  arrEneter[6]=6
entero =  7  arrEneter[7]=7
entero =  8  arrEneter[8]=8
entero =  9  arrEneter[9]=9
entero =  27  arrEneter[27]=0
entero =  11  arrEneter[11]=11
entero =  12  arrEneter[12]=12

Vector de cadena de caracteres.
cadena:String;
arrCadena:array[1..5] of String =('Lunes','Martes','Miércoles','Jueves','Viernes');
for cadena in arrCadena do WriteLn('Cadena = ',cadena);
Cadena = Lunes
Cadena = Martes
Cadena = Miércoles
Cadena = Jueves
Cadena = Viernes

Enumerados.
TSemana=(domingo, lunes, martes, miercoles, jueves, viernes, sabado);
dia:TSemana;
for dia in Tsemana do WriteLn('dia = ',dia);
dia = domingo
dia = lunes
dia = martes
dia = miercoles
dia = jueves
dia = viernes
dia = sabado

Conjuntos.
TSemana=(domingo, lunes, martes, miercoles, jueves, viernes, sabado);
diaslaborables:set of TSemana =[lunes, martes, miercoles, jueves, viernes];
dia:TSemana;
for dia in diaslaborables do WriteLn('dia = ',dia);
dia = lunes
dia = martes
dia = miercoles
dia = jueves
dia = viernes

Caracteres.
caracter:Char;
for caracter in 'abcdefg' do WriteLn('caracter = ',caracter);
caracter = a
caracter = b
caracter = c
caracter = d
caracter = e
caracter = f
caracter = g

Classes.
cadena:String;
meses:TStringList;
for cadena in meses do WriteLn('cadena = ',cadena);
cadena = Enero
cadena = Febrero
cadena = Marzo
cadena = Abril
cadena = Mayo

Para ejemplos más avanzados y documentación oficial:

Documentación: https://www.freepascal.org/docs-html/ref/refsu59.html

Wiki (Siempre y cuando algún iluminado no haya eliminado o movido la página): https://wiki.lazarus.freepascal.org/for-in_loop

Código fuente del ejemplo.

domingo, 22 de agosto de 2021

SetFocus y un error muy común.

Casi todos los componentes visuales de Lazarus tienen un procedimiento llamado SetFocus, por lo que es normal que si tenemos 10 TEdit en un Form, queramos que el primero de ellos tenga el "focus", y luego el segundo y así sucesivamente. Y ahí el dicho "code first, think later" o "escribe el código primero, piensa después" se pone de manifiesto cuando intentamos un Edit1.SetFocus en el método FormCreate y tenemos un hermoso error en tiempo de ejecución. ¿Por qué?

[TCustomForm.SetFocus] Form1:TForm1 Can not focus.

Justamente porque pedimos poner el foco en algo que todavía no existe, el Edit1, en el evento FormCreate que hace lo que dice, crea el formulario, los TEdit todavía no existen.

Sí, todo muy lindo y suena hasta lógico, ¿pero entonces cómo lo consigo?

Opción 1: usando la opción "Orden de tabulación", click derecho sobre el formulario.

Opción 2: precisamente en el procedimiento FormCreate estableciendo el control activo de la siguiente forma: 

procedure TForm1.FormCreate(Sender: TObject);
begin
  ActiveControl:=Edit1;
end; 

Y la duda razonable, si no se puede un setfocus ¿por qué sí un ActiveControl?, la respuesta está en el código fuente de la unidad Forms (forms.pp), wincontrol.inc y customform.inc. La respuesta corta es que edit1.setfocus aún no está disponible porque estamos llamando a un evento de un TEdit en FormCreate, en cambio ActiveControl es una propiedad de TForm.

En el código de la unidad control.pp podemos encontrar:

    function CanFocus: Boolean; virtual;
    function CanSetFocus: Boolean; virtual;  

que son funciones públicas de la clase (class) TWinControl que hereda de TControl y también pueden ser usadas para evitar errores al utilizar el procedimiento SetFocus.

Otras opciones:

procedure TForm1.FormActivate(Sender: TObject);
begin
  Edit1.SetFocus;
end;    

procedure TForm1.FormShow(Sender: TObject);
begin
 Edit1.SetFocus;
end;    

Funcionan bien con formularios mostrados mediante ShowModal, no obstante recomiendo las dos primeras.

lunes, 16 de agosto de 2021

Registros de estructura variable.

A diferencia de los registros de estructura fija, los registros de estructura variable tienen campos disponibles según el resultado del selector CASE OF. No es algo simple de comprender a primera vista por ende lo más recomendable es practicar (y mucho) para poder entender como funciona.
Es usado en programación avanzada, en el código fuente de Turbo Pascal, por ejemplo, y calculo que en Free Pascal también. Vale aclarar que no se recomienda su uso, además de por lo complejo, por tratarse Pascal de un lenguaje fuertemente tipado. Si bien existen al menos desde Turbo Pascal, sospecho que su implementación fue para tener algo parecido a “union” de otro lenguaje. Con Object Pascal, hay muy buenas alternativas, advanced records, objects y class (registros avanzados, objetos y clases).

En mi caso me costaba entender como hacía el compilador, ya que según el CASE luego podrían venir 10 campos más, o 2 o ninguno, porque supuestamente es una estructura variable, pero no es tan variable, es una unión, ahí está la clave, lo variable es como se lo utiliza, pero el record se arma con todos lo campos, si hay un CASE con un selector que define 3 campos y otro define 5 campos, entonces el registro tendrá 8 campos, no 3 u 8 según el valor del selector. Claro que algunos campos compartirán la misma área de memoria. La longitud (máxima) de los campos de la parte variante debe ser conocida por el compilador, por ejemplo, Byte, Integer, string[20] está bien, en cambio string, no.

Por ejemplo:

type
  Reg=record
    campo1:string[5];
    case algo: boolean of:
    true : (campo2: integer; campo3:Byte);
    false: (campo4:word);
  end;

Vendría internamente a ser algo así:

type
  Reg=record
    campo1:string[5];
    campo2: integer;
    algo:boolean;
    campo3:Byte;
    campo4:word;
  end;

Pero hay algo más complicado aún, todos los campos son accesibles independientemente del selector, es decir, el programador es el responsable de por ej., si algo es false entonces no utilizar el campo2 y el campo3, solo el campo4.

type TRec=record
       int:Integer;
       case Boolean of
       True : (s1:String[10]);
       False: (b1:Byte);
     end;

En este caso, no es posible establecer el valor de Boolean.
¿Qué dice la documentación oficial al respecto?
La parte variante debe ser la última en el registro. El identificador opcional en la instrucción case sirve para acceder al valor del campo de etiqueta, que de otro modo sería invisible para el programador. Se puede utilizar para ver qué variante está activa en un momento determinado (sin embargo, depende del programador mantener este campo). En efecto, introduce un nuevo campo en el registro.

Pero hay más, en alguno casos, como veremos en un ejemplo de consola, la parte variante para determinados identificadores o campos, comparten la misma área de memoria, por ende el valor es el mismo independientemente de valor del selector. Según alguien escribió en la wiki de FP, esto es algo extremadamente útil y cita un ejemplo con una declaración de registro y una breve explicación.
El ejemplo es el siguiente:

type
  TSpecialWord = record
    case Byte of
      0: (Word_value: Word);                      // 7  type Word = 0..65535;
      1: (Byte_low, Byte_high: Byte);             // 7, 0 type Byte = 0..255
      2: (Bits: bitpacked array [0..15] of 0..1); // 1, 1, 1, 0, 0, ...
  end;

Este registro tiene solo una parte variable y permite el acceso al valor de la palabra, los bytes individuales e incluso a los bits. Un identificador no es necesariamente necesario en la cláusula case, por lo que no ocupa ninguna memoria. El tamaño de este registro es de dos bytes. En el caso de Bits, esto solo es posible si se utiliza bitpacked. Observe el orden de los bytes, con el byte menos significativo (LSB) primero.

Veamos que pasa con ese ejemplo.
Declaramos una variable sw:TSpecialWord. No inicializamos la variable e imprimimos con writeln. El resultado serán todos ceros. Lo mismo sucede si incicializamos con Deflaut (sw:=Default(TSpecialWord);).
Ahora agregamos:  

sw.Word_value:=4;

Resultado:

Word_value = 4  //Lo asignamos.
Byte_low = 4  //Comparte la misma dirección de memoria, es correcto.
Byte_high = 0  //Es el valor por default.
Bits = 0010000000000000 //Es 4 en binario.

Probemos con:

sw.Word_value:=40000;

Resultado:

Word_value = 40000
Byte_low = 64  //40000/625=64 o 40000/156,25*4=64
Byte_high = 156 //40000/256=156,25
Bits = 0000001000111001 //569 en decimal.

Más pruebas:

sw.Bits[0]:=1;
sw.Bits[1]:=1;
sw.Bits[2]:=1;

Resultado:

Word_value = 7
Byte_low = 7
Byte_high = 0
Bits = 1110000000000000 //7

sw.Byte_low:=5;

Resultado:

Word_value = 5
Byte_low = 5
Byte_high = 0
Bits = 1010000000000000 //5

sw.Byte_high:=128;

Resultado:

Word_value = 32768
Byte_low = 0
Byte_high = 128
Bits = 0000000000000001

sw.Byte_low:=128;

Word_value = 128
Byte_low = 128
Byte_high = 0
Bits = 0000000100000000 //256

sw.Byte_high:=8;

Resultado:

Word_value = 2048
Byte_low = 0
Byte_high = 8
Bits = 0000000000010000 //16

sw.Bits[0]:=1;
sw.Bits[1]:=1;
sw.Bits[2]:=1;
sw.Bits[3]:=1;
sw.Bits[4]:=1;
sw.Bits[5]:=1;
sw.Bits[6]:=1;
sw.Bits[7]:=1;
sw.Bits[8]:=1;  

Resultado:

Word_value = 511
Byte_low = 255
Byte_high = 1
Bits = 1111111110000000 //511

Como vemos es bastante complejo cuando no se utiliza un identificador en el selector CASE.

El identificador del selector debe ser del tipo ordinal.

Los Case se pueden anidar.

Las partes variantes no pueden ser del tipo variant, string largos (string no, string[120] sí),  vectores dinámicos, datos estructurados que contengan lo anteriormente citado o interfaces, pero sí pueden ser punteros a esos tipos (types).

La mejor documentación que encontré es de Embarcadero: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Structured_Types_(Delphi)#Variant_Parts_in_Records

Como no encontré ningún ejemplo desarrollado completo elaboré uno.
 
Código fuente: RegistrosVariantes.7z
unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls,
  DateTimePicker;

type

  { TForm1 }

  TForm1 = class(TForm)
    BMostrarRegVariante: TButton;
    BCerrar: TButton;
    cbCasado: TCheckBox;
    DateTimePicker1: TDateTimePicker;
    Label1: TLabel;
    Label2: TLabel;
    Memo2: TMemo;
    procedure BMostrarRegVarianteClick(Sender: TObject);
    procedure BCerrarClick(Sender: TObject);
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure FormCreate(Sender: TObject);
  private

  public

  end;

  TVariantRec=record
    ID:Word;
    Nombre:String[50];
    case Casado : Boolean of
      True : (vrDate:TDate);
      False: (str2:String[10]);
  end;

var
  Form1: TForm1;
  VariantRec:TVariantRec;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
begin
  VariantRec.ID:=27;
  VariantRec.Nombre:='Jorge';
  VariantRec.Casado:=True;
  VariantRec.vrDate:=EncodeDate(2010,12,25);
  VariantRec.str2:='Soltero';
end;

procedure TForm1.BMostrarRegVarianteClick(Sender: TObject);
begin
  VariantRec.Casado:=cbCasado.Checked;
  if cbCasado.Checked then
    VariantRec.vrDate:=DateTimePicker1.Date
  else
    VariantRec.str2:='Soltero';
  Memo2.Lines.Add(VariantRec.ID.ToString);
  Memo2.Lines.Add(VariantRec.Nombre);
  Memo2.Lines.Add(BoolToStr(VariantRec.Casado,'True','False'));
  Memo2.Lines.Add(DateToStr(VariantRec.vrDate));
  Memo2.Lines.Add(VariantRec.str2);
end;

procedure TForm1.BCerrarClick(Sender: TObject);
begin
  Close;
end;

procedure TForm1.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
  CloseAction:=caFree;
end;

end.  

Más ejemplos, en este caso por consola:

program project1;

{$mode objfpc}{$H+}

uses
  {$IFDEF UNIX}{$IFDEF UseCThreads}
  cthreads,
  {$ENDIF}{$ENDIF}
  Classes
  { you can add units after this };

type TRec=record
       int:Integer;
       case Boolean of
       True : (s1:String[10]);
       False: (b1:Byte);
     end;
var
  rec:TRec;

begin
  rec.int:=2;
  rec.s1:='abx';
  rec.b1:=4;

  WriteLn(rec.int);
  WriteLn(rec.b1);
  WriteLn(rec.s1);
  ReadLn;
end.

Resultado:                   

2
4
abx

Todo, como un registro fijo.
 
-------------------------------------------------------------------------------------------------------------------------- 

type TRec=record
       int:Integer;
       case cond:Boolean of
       True : (s1:String[10]);
       False: (b1:Byte);
     end;
var
  rec:TRec;

begin
  rec.int:=2;
  rec.cond:=False;
  rec.s1:='abx';
  rec.b1:=4;

  WriteLn(rec.int);
  WriteLn(rec.cond);
  WriteLn(rec.b1);
  WriteLn(rec.s1);
  ReadLn;
end.       

Resultado:                   

2
FALSE
4
abx

De nuevo, todo, al compartir la misma dirección de memoria, entiendo que no debería suceder, o dar en s1 otro resultado que sea abx. Tampoco debí pedir el campo s1.
 
--------------------------------------------------------------------------------------------------------------------------

type TRec=record
       int:Integer;
       case cond:Boolean of
       True : (s1:String[10]);
       False: (s2:String[10]; b1:Byte);
     end;
var
  rec:TRec;

begin
  rec.int:=2;
  rec.cond:=False;
  rec.s1:='abx';
  rec.s2:='fgh';
  rec.b1:=4;

  WriteLn(rec.int);
  WriteLn(rec.cond);
  WriteLn(rec.s1);
  WriteLn(rec.s2);
  WriteLn(rec.b1);
  ReadLn;
end.   

Resultado:                   

2
FALSE
fgh
fgh
4

En este caso si se muestra el mismo valor en s1 y s2, mismo tipo, imprime el valor de s2 porque cond=flase. 
 
Documentación:

viernes, 6 de agosto de 2021

La directiva {$include}

Tal como lo indica su nombre, la directiva de compilación {$Include} o {$I} se utiliza para incluir el contenido de un archivo. Generalmente se utiliza la extensión de archivo .inc aunque es opcional, también se utiliza .pp y .pas o puede no utilizarse ninguna extensión.

Si nos fijamos en el código fuente de Lazarus o de Free Pascal, encontraremos miles de ejemplos.

El uso de archivos ".inc" mejora mucho la legibilidad de un proyecto y su mantenimiento. Muchas veces se utiliza para separar el código que "molesta", como gran cantidad de definiciones. Por ejemplo una unidad que utiliza muchas definiciones de registros que ocupan varias líneas (100, 200, lo que cada uno entienda por "varias"), esas definiciones se pueden separar a otro archivo .inc e invocar ese archivo mediante la directiva {$I} en una sola línea en el lugar donde antes estaban, es importante esto, porque el compilador traerá lo que contenga el archivo .inc y lo insertará donde se encuentre la directiva.

En este caso serán definiciones de registros pero puede ser cualquier cosa, procedimientos, clases, etc. 

No es necesario incluir el archivo al proyecto, el compilador lo buscará en el directorio del proyecto, en los directorios a buscar indicados en el proyecto o en el lugar que se haya especificado, por ejemplo: {$I /home/programas/prueba/archivo.inc"}.

Se puede invocar esta directiva en una unidad todas las veces que sea necesario y en cualquier parte de la unidad, es decir que podemos incluir varios archivos en distintas partes.

No solo se pueden incluir archivos, también información de compilación y variables de entorno, en este último caso se diferencia entre mayúsculas y minúsculas.

Documentación oficial (en inglés) de esta directiva:

$I or $INCLUDE : Include file 

$I or $INCLUDE : Include compiler info

Wiki de Free Pascal.