Article Series

This article series discuss more than 30 different programming languages. Please read overview before you read any of the details.

Playing with Records Related Articles.

Where to Discuss?

Local Group

Preface

Goal: A practical case to collect unique record fields using TCL/TK.

Just like anyone else, I don’t know anything about TCL/TK. What is it use for, except for expect test suite in LFS. But I’m curious, so here it is anyway.

Reference Reading

I never read any TCL documentation, until the time this article written.

Source Examples

You can obtain source examples here:


Common Use Case

Task: Get the unique tag string

Please read overview for more detail.

Prepopulated Data

Songs and Poetry

set ::MySongs::Songs [list \
  [ dict create \
    title "Cantaloupe Island" \
    tags  [list "60s" "jazz"] ] \
  [ dict create \
    title "Let It Be" \
    tags  [list "60s" "rock"] ] \
  [ dict create \
    title "Knockin' on Heaven's Door" \
    tags  [list "70s" "rock"] ] \
  [ dict create \
    title "Emotion" \
    tags  [list "70s" "pop"] ] \
  [ dict create title "The River" ]
];

TCL Solution

The Answer

I use conventional building blocks, loop and conditional.

set tags {}

foreach song $songs {
  if [dict exist $song tags] {
    foreach tag [dict get $song tags] {
      if {[lsearch $tags $tag] < 0} {
        lappend tags $tag
}}}}

puts "[join $tags ":"]"

Enough with introduction, at this point we should go straight to coding.

Environment

The command is tclsh.

No need any special setup. Just run and voila..!


1: Data Structure Using Dictionary

We utilize list, throught out this article.

Simple List

Consider begin with simple list.

#!/usr/bin/env tclsh

set tags [list "rock" "jazz" "rock" "pop" "pop"];
puts $tags;
puts [lindex $tags 1]

set tagsstr [join $tags :];
puts "$tagsstr\n";

puts [llength $tags]

It is easy to dump list in tcl using join. With the result similar as below list:

❯ tclsh 01-tags.tcl
rock jazz rock pop pop
jazz
rock:jazz:rock:pop:pop

5

TCL/TK: A very simple list

Notice how the assignment written in TCL.

set tagsstr [join $tags ":"];

Shebang

With shebang such as #!/usr/bin/env tclsh, you do not need to type tcl in CLI.

Instead of

❯ tclsh 02-record.tcl

You can just type

❯ ./02-record.tcl

And you can run the script directly in your text editor. Such as using geany, you just can hit the F5 key to run the script.

Hash

We can use hash to store our record.

set song [ \
  dict create \
  title "Cantaloupe Island" \
  tags  [list "60s" "jazz"] \
]

And examine how to access the array inside the hash.

puts $song

set title [dict get $song title]
puts $title

set tags [dict get $song tags]
puts $tags
puts [lindex $tags 0]

Now, examine the result:

title {Cantaloupe Island} tags {60s jazz}
Cantaloupe Island
60s jazz
60s

TCL/TK: Using Hash

The Songs Structure

We can continue our journey to records using list of hash. No need any complex structure.

set songs [list \
  [ dict create \
    title "Cantaloupe Island" \
    tags  [list "60s" "jazz"] ] \
  [ dict create \
    title "Let It Be" \
    tags  [list "60s" "rock"] ] \
  [ dict create \
    title "Knockin\' on Heaven\'s Door" \
    tags  [list "70s" "rock"] ] \
  [ dict create \
    title "Emotion" \
    tags  [list "70s" "pop"] ] \
  [ dict create title "The River" ]
];

Then process in a loop to produce desired output.

puts [llength $songs] 

foreach song $songs {
  puts $song
}

With the result similar as below record:

❯ ./03-songs.tcl
5
title {Cantaloupe Island} tags {60s jazz}
title {Let It Be} tags {60s rock}
title {Knockin' on Heaven's Door} tags {70s rock}
title Emotion tags {70s pop}
title {The River}

TCL/TK: Songs Record


2: Separating Module

Since we need to reuse the songs record multiple times, it is a good idea to separate the record structure from logic.

We require two files.

  1. One or more module, such as MySongs.tcl.
  2. Package Index: pkgIndex.tcl

Songs Module

The header for MySongs module should be as below:

namespace eval ::MySongs {
  namespace export Songs

  set version 1.0
  set MyDescription "MySongs"
  variable home [file join [pwd]\
    [file dirname [info script]]]
}

TCL/TK: Songs Module: Header

Then the code details, can be shown as below:

set ::MySongs::Songs [list \
  [ dict create \
    title "Cantaloupe Island" \
    tags  [list "60s" "jazz"] ] \
  [ dict create \
    title "Let It Be" \
    tags  [list "60s" "rock"] ] \
  [ dict create \
    title "Knockin' on Heaven's Door" \
    tags  [list "70s" "rock"] ] \
  [ dict create \
    title "Emotion" \
    tags  [list "70s" "pop"] ] \
  [ dict create title "The River" ]
];

package provide MySongs $MySongs::version
package require Tcl 8.0

TCL/TK: Songs Module: Array of Records

Package Index

We still need to configure package index for MySongs module.

package ifneeded MySongs 1.0 \
  [list source [file join $dir MySongs.tcl]]

TCL/TK: Package Index

Module in Relative Path

In order to use module in relative path, we need to add a few more header declaration.

lappend auto_path "./"
package require MySongs 1.0

Then we can just dump the $Songs variable.

#!/usr/bin/env tclsh

lappend auto_path "./"
package require MySongs 1.0

puts $MySongs::Songs

With the result as below image:

TCL/TK: Using Songs Module


3: Finishing The Task

Extract, Flatten, Unique

We can solve, this task, by using imperative approach, such as for loop and if conditional.

Extracting Hash

Filtering hash, and push to array

Consider start with this simple code:

#!/usr/bin/env tclsh

lappend auto_path "./"
package require MySongs 1.0

foreach song $MySongs::Songs {
  if [dict exist $song tags] {
    set tagss [dict get $song tags]
    puts $tagss
  }
}

With the result of list of list, as shown below.

❯ ./05-extract.tcl
60s jazz
60s rock
70s rock
70s pop

TCL/TK: Extracting Hash Records

Flatten Module

Since we are going to reuse the flatten approach above in other script. It is better to bundle the script in its own tcl module.

namespace eval ::MyHelperFlatten {
  namespace export unique

  set version 1.0
  set MyDescription "MyHelperFlatten"
  variable home [file join [pwd]\
    [file dirname [info script]]]
}

proc flatten {songs} { ... }

package provide MyHelperFlatten $MyHelperFlatten::version
package require Tcl 8.0

The return values is an array.

TCL/TK: The Flatten Module

Package Index

We still need to configure package index for MySongs module.

package ifneeded MySongs 1.0 \
  [list source [file join $dir MySongs.tcl]]

package ifneeded MyHelperUnique 1.0 \
  [list source [file join $dir MyHelperUnique.tcl]]

Flatten Function

To flatten the code above, we can directly push array with this code below:

proc flatten {songs} {
  set tags [list]

  foreach song $songs {
    if [dict exist $song tags] {
      set tagss [dict get $song tags]
      foreach tag $tagss {
        lappend tags $tag
      }
    }
  }

  return $tags
}

TCL/TK: The Flatten Module

Using Flatten Module

There is nothing to say in this code below. Just apply flatten function to our song records.

#!/usr/bin/env tclsh

lappend auto_path "./"
package require MySongs 1.0
package require MyHelperFlatten 1.0

set tags [flatten $MySongs::Songs]
set tagsstr [join $tags ":"]
puts "$tagsstr"

With the result of a flattened array shown below.

❯ ./06-flatten.tcl
60s:jazz:60s:rock:70s:rock:70s:pop

Unique

To solve unique array, we can directly check unique value whule filtering.

#!/usr/bin/env tclsh

lappend auto_path "./"
package require MySongs 1.0

set tags [list]

foreach song $MySongs::Songs {
  if [dict exist $song tags] {
    set tagss [dict get $song tags]
    foreach tag $tagss {
      if {[lsearch $tags $tag] < 0} {
        lappend tags $tag
}}}}

set tagsstr [join $tags ":"];
puts "$tagsstr";

With the result similar as below array:

❯ ./07-unique.tcl
60s:jazz:rock:70s:pop

TCL/TK: Solving Unique Song


What is Next 🤔?

We have alternative way to extract the record structure.

Consider continue reading [ TCL - Playing with Records - Part Two ].