Strain in Arturo: Complete Solution & Deep Dive Guide


Arturo Strain from Zero to Hero: The Definitive Guide to Collection Filtering

The "Strain" operation in Arturo, implemented as keep and discard functions, provides a powerful, declarative way to filter collections. It works by applying a predicate function to each element, creating new collections containing only the elements that either satisfy (keep) or fail (discard) the given condition.


The Data Deluge: Why Filtering is Your Most Important Skill

Imagine you're handed a massive dataset—thousands of user records, millions of log entries, or a long list of financial transactions. Your task is to extract meaningful information. You need to find all the active users, isolate the critical error messages, or identify transactions above a certain value. How do you approach this without getting lost in a sea of data?

This is a universal challenge in programming. The process of sifting, sorting, and selecting data is fundamental. Many developers initially reach for cumbersome loops and complex conditional statements, leading to code that is hard to read, difficult to maintain, and prone to bugs. You've likely felt this pain: the nested if statements, the temporary arrays, the nagging feeling that there must be a better way.

This is where the concept of "Strain" comes in. It’s a powerful idea from functional programming that changes the way you think about data manipulation. Instead of telling the computer how to loop through every single item, you simply declare what you want. In this comprehensive guide, we'll explore how to master this concept in Arturo, using the elegant keep and discard operations. You'll learn to write cleaner, more expressive, and more efficient code, transforming you from a data wrangler into a data artist.


What is the "Strain" Operation?

At its core, the "Strain" operation is about filtering a collection of items based on a specific test or condition. This test is encapsulated in something called a predicate. A predicate is simply a function that takes an element as input and returns a boolean value: true or false.

The operation is split into two complementary actions:

  • Keep: This action iterates through a collection and builds a new collection containing only the elements for which the predicate returns true. It keeps the "good" data.
  • Discard: This action does the opposite. It iterates through the collection and builds a new collection containing only the elements for which the predicate returns false. It gets rid of the "unwanted" data.

A crucial aspect of this pattern is immutability. The original collection is never modified. Instead, these operations always return a new, filtered collection. This is a cornerstone of safe and predictable programming, as it prevents unintended side effects that can occur when data is modified in place.

This approach is a form of declarative programming. You declare your intent ("I want all the even numbers") rather than spelling out the imperative steps ("Create an empty list. Loop from the first to the last number. For each number, check if it's divisible by two. If it is, add it to the empty list. Finally, return the list."). This leads to code that is significantly more readable and concise.


Why is Declarative Filtering a Game-Changer in Arturo?

Adopting the keep and discard pattern isn't just about writing less code; it's about adopting a more powerful and modern programming paradigm. Arturo, with its expressive syntax and functional capabilities, is perfectly suited for this style.

The Four Pillars of Effective Filtering

  1. Enhanced Readability: Code like keep users isActive is self-documenting. It reads like plain English and immediately conveys its purpose. Compare this to a multi-line loop with an if block inside—the intent is much clearer in the declarative version.
  2. Guaranteed Immutability: By never altering the original data source, you eliminate a whole class of bugs related to state management. Your functions become pure and predictable: given the same input, they will always produce the same output without side effects.
  3. Improved Composability: Functional operations like these are designed to be chained together. You can take the result of a keep operation and immediately pass it to another function, creating elegant data processing pipelines. For example: map (keep users isActive) 'getName.
  4. Separation of Concerns: The logic for what to filter (the predicate) is completely separate from the logic for how to filter (the keep/discard implementation). This makes your code more modular and easier to test. You can test your predicate functions in isolation.

In essence, mastering this concept elevates your code from a simple list of instructions to a sophisticated expression of logic, making your programs more robust, scalable, and easier for you and your team to understand.


How to Implement `keep` and `discard` in Arturo

Let's dive into the practical implementation. The goal is to create two functions, keep and discard, that take a collection and a predicate function as arguments. Fortunately, Arturo's standard library is rich with functional helpers that make this incredibly straightforward.

We will leverage Arturo's built-in select and reject functions, which correspond directly to our desired keep and discard behaviors, respectively. We'll wrap them in our named functions to align with the terminology from the kodikra learning path.

The Complete Solution Code

Here is the full, commented code that solves the Strain problem. It includes the function definitions and example usage with different data types to showcase its versatility.


; =======================================================
; Strain Implementation (Keep & Discard) for kodikra.com
; Language: Arturo
; =======================================================

; -------------------------------------------------------
; Core Function Definitions
; -------------------------------------------------------

; The 'keep' function.
; Takes a collection (e.g., a list) and a predicate function.
; It returns a NEW list containing only the elements for which
; the predicate returns `true`.
; We use Arturo's built-in `select` for this task.
keep: function [coll predicate][
    select coll predicate
]

; The 'discard' function.
; Takes a collection and a predicate function.
; It returns a NEW list containing only the elements for which
; the predicate returns `false`.
; We use Arturo's built-in `reject` for this, as it's the
; logical inverse of `select`.
discard: function [coll predicate][
    reject coll predicate
]


; -------------------------------------------------------
; Example Usage & Demonstration
; -------------------------------------------------------

print "\n--- Example 1: Filtering Numbers ---"

; Define a sample collection of integers
numbers: [1 2 3 4 5 6 7 8 9 10]
print ["Original numbers:" numbers]

; Define a predicate to check if a number is even.
; A predicate is just a function that returns true or false.
isEven: function [n][
    0 = n % 2
]

; -- Test the 'keep' function --
evenNumbers: keep numbers isEven
print ["Keeping even numbers:" evenNumbers]
; Expected output: -> [2 4 6 8 10]

; -- Test the 'discard' function --
; Discarding even numbers is the same as keeping odd numbers.
oddNumbers: discard numbers isEven
print ["Discarding even numbers:" oddNumbers]
; Expected output: -> [1 3 5 7 9]


print "\n--- Example 2: Filtering Strings ---"

; Define a sample collection of strings
words: ["arturo" "language" "functional" "filtering" "awesome"]
print ["\nOriginal words:" words]

; Define a predicate to check if a word has more than 8 letters.
isLongWord: function [word][
    (size word) > 8
]

; -- Test the 'keep' function with strings --
longWords: keep words isLongWord
print ["Keeping long words (>8 letters):" longWords]
; Expected output: -> ["language" "functional" "filtering"]

; -- Test the 'discard' function with strings --
shortWords: discard words isLongWord
print ["Discarding long words (>8 letters):" shortWords]
; Expected output: -> ["arturo" "awesome"]

Detailed Code Walkthrough

Let's break down the logic piece by piece to understand exactly what's happening.

1. The `keep` Function

The keep function is our tool for selecting elements we want to retain. Its logic flows as follows:

● Start with a collection & a predicate

    │
    ▼
┌──────────────────┐
│  Function `keep` │
│ (coll, predicate)│
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│  Internally calls│
│ `select coll ...`│
└────────┬─────────┘
         │
         ├─ For each element in `coll`...
         │
         ▼
   ◆ Apply predicate(element)?
   ╱                           ╲
 `true`                       `false`
  │                              │
  ▼                              ▼
┌──────────────┐              ┌───────────┐
│ Add element to│              │ Ignore the│
│ new result list│             │ element   │
└──────────────┘              └───────────┘
         │                              │
         └──────────────┬───────────────┘
                        │
                        ▼
┌──────────────────────────────┐
│ Return the new, filtered list│
└──────────────────────────────┘
                        │
                        ▼
                     ● End

The implementation keep: function [coll predicate][ select coll predicate ] is a simple wrapper. The real work is done by Arturo's select function. It iterates over coll, applies the predicate to each item, and if the predicate returns true, the item is included in the new list that select returns.

2. The `discard` Function

The discard function is the mirror image of keep. It's for getting rid of elements that meet a certain condition.

● Start with a collection & a predicate

    │
    ▼
┌─────────────────────┐
│ Function `discard`  │
│  (coll, predicate)  │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│  Internally calls   │
│ `reject coll ...`   │
└──────────┬──────────┘
           │
           ├─ For each element in `coll`...
           │
           ▼
     ◆ Apply predicate(element)?
     ╱                           ╲
   `true`                       `false`
    │                              │
    ▼                              ▼
┌───────────┐                   ┌──────────────┐
│ Ignore the│                   │ Add element to│
│ element   │                   │ new result list│
└───────────┘                   └──────────────┘
    │                              │
    └──────────────┬───────────────┘
                   │
                   ▼
┌──────────────────────────────┐
│ Return the new, filtered list│
└──────────────────────────────┘
                   │
                   ▼
                ● End

Similarly, our discard function uses Arturo's built-in reject. The reject function is the logical opposite of select. It iterates over the collection and includes an element in its result only if the predicate returns false (or nil). This makes it perfect for the "discard" operation.

3. The Predicates (`isEven`, `isLongWord`)

These are the heart of the filtering logic. A predicate is a specialized function that does one thing: it evaluates an item and returns true or false.

  • isEven: function [n][ 0 = n % 2 ]: This function takes a number n. The modulo operator % gives the remainder of a division. If n % 2 is 0, the number is even, and the expression 0 = ... evaluates to true. Otherwise, it's false.
  • isLongWord: function [word][ (size word) > 8 ]: This takes a string word. The size function returns its length. The expression then checks if this length is greater than 8, returning true or false accordingly.

By defining these simple, focused predicates, we can reuse them in multiple keep or discard calls, promoting clean and DRY (Don't Repeat Yourself) code.


Where to Use Strain: Real-World Scenarios

This pattern is not just an academic exercise; it's used constantly in professional software development. Here are some practical examples of where you might use keep and discard.

Scenario 1: E-commerce Product Management

Imagine you have a list of product objects, and you need to display only the ones that are in stock and cost less than $50.


products: [
    @{ name: "Laptop", price: 1200, inStock: true }
    @{ name: "Mouse", price: 25, inStock: true }
    @{ name: "Keyboard", price: 75, inStock: false }
    @{ name: "Webcam", price: 45, inStock: true }
]

; Predicate to find affordable, in-stock items
isAffordableAndAvailable: function [p][
    and p\inStock (p\price < 50)
]

; Get the list of products to display
displayProducts: keep products isAffordableAndAvailable

; displayProducts is now:
; [ @{ name: "Mouse", price: 25, inStock: true }, @{ name: "Webcam", price: 45, inStock: true } ]

Scenario 2: Data Cleaning and Validation

You receive a list of email addresses from a form submission, but some are invalid or empty. You need to clean the list before saving it to the database.


submissions: ["user@example.com", "", "   ", "another@test.com", "invalid-email"]

; A simple predicate to check for valid-looking emails
isValidEmail: function [email][
    and (not empty? trim email) (contains? email "@")
]

; Keep only the valid emails
cleanEmails: keep submissions isValidEmail

; cleanEmails is now: ["user@example.com", "another@test.com"]

Scenario 3: Processing Sensor Data

You are working with an IoT device that sends temperature readings. You need to discard any readings that are clear outliers (e.g., below freezing or above boiling) before calculating the average.


readings: [22.5, 23.1, -99.0, 22.8, 101.2, 23.5]

; Predicate to identify readings within a sane range (0 to 100 Celsius)
isSaneReading: function [temp][
    and (temp >= 0) (temp <= 100)
]

; Use 'keep' to get only the valid readings
validReadings: keep readings isSaneReading

; validReadings is now: [22.5, 23.1, 22.8, 23.5]

Alternative Approaches & Performance Considerations

While the functional approach with keep and discard is highly recommended for its clarity, it's useful to compare it to the more traditional, imperative approach using a loop.

The Imperative Way: Manual Looping

Here's how you might implement the "keep even numbers" logic using a manual loop:


numbers: [1 2 3 4 5 6 7 8 9 10]
evenNumbers: [] ; Start with an empty list

loop numbers 'n [
    if 0 = n % 2 [
        ; Manually add the element to our results
        'evenNumbers ++ n
    ]
]

print evenNumbers ; -> [2 4 6 8 10]

Comparison: Functional vs. Imperative

Let's put them side-by-side to highlight the differences.

Aspect Functional (keep/discard) Imperative (manual loop)
Readability High. keep numbers isEven clearly states intent. Lower. Requires reading multiple lines to understand the logic.
Verbosity Low. Extremely concise. High. Requires manual list initialization, looping, and appending.
State Management Stateless and immutable. The original list is untouched. Stateful. You must create and manage a mutable evenNumbers list.
Risk of Errors Low. Less boilerplate code means fewer places for bugs to hide. Higher. Risk of off-by-one errors, mutation bugs, or incorrect loop logic.
Performance Highly optimized. Built-in functions are often implemented in a lower-level, faster language. Can be fast, but performance depends on the implementation. For most cases, the difference is negligible.

For the vast majority of use cases, the performance of built-in functions like select and reject will be excellent and often better than a hand-rolled loop in a high-level language. The benefits in readability, maintainability, and safety almost always outweigh any micro-optimizations you might gain from a manual loop, unless you are working with millions of elements in a performance-critical path.


Frequently Asked Questions (FAQ)

1. What exactly is a "predicate" in Arturo?

A predicate is a function that accepts one argument and returns a boolean (true or false) value. Its purpose is to act as a test or condition. In our examples, isEven and isLongWord are predicates because they test an input and confirm or deny a property about it.

2. Are `keep` and `discard` destructive? Do they modify my original list?

No, they are non-destructive and follow the principle of immutability. Both keep and discard (and the underlying select/reject functions) always return a new list. Your original collection remains completely unchanged, which is a much safer way to program.

3. Can I use these filtering operations on other collection types, like dictionaries?

Yes! Arturo's select and reject are versatile. When used on a dictionary (table), the predicate function will receive two arguments: the key and the value. You can then filter based on either. For example: keep myDict [k,v][(size k) > 3] would keep only the key-value pairs where the key's length is greater than three.

4. How is this functionally different from just using a `loop` with an `if` statement?

While the end result might be the same, the approach is fundamentally different. A loop is imperative—you tell the computer the exact steps to perform. Using keep/discard is declarative—you describe the result you want, and the language figures out the steps for you. This leads to more abstract, readable, and maintainable code.

5. What is a "higher-order function" and how does it relate to this?

A higher-order function is a function that either takes another function as an argument, returns a function, or both. keep and discard are perfect examples of higher-order functions because they take the predicate function as one of their arguments. This is a core concept in functional programming.

6. Can I chain multiple filtering operations together?

Absolutely! This is one of the biggest advantages of this pattern. You can create powerful data processing pipelines. For example: keep (keep numbers isEven) [x][x > 5] would first get all even numbers, and from that result, keep only the ones greater than 5, resulting in [6 8 10].

7. Where can I learn more about functional programming in Arturo?

The official documentation is a great starting point. For a structured learning experience, you can explore the complete Arturo guide on kodikra.com, which covers these concepts in depth as part of a comprehensive curriculum.


Conclusion: Embrace the Functional Mindset

You've now seen the power and elegance of the Strain pattern in Arturo. By leveraging the declarative nature of keep (select) and discard (reject), you can write code that is not only more concise but also more readable, robust, and easier to reason about. You've moved beyond simple loops and embraced higher-order functions—a critical step in becoming a more effective programmer.

Remember the key takeaways:

  • Declare, Don't Command: Focus on describing what you want, not how to get it.
  • Embrace Immutability: Protect your original data by always creating new, transformed collections.
  • Separate Logic: Keep your filtering conditions (predicates) separate from your filtering mechanism for clean, modular code.

As you continue your journey through the kodikra Arturo learning path, you'll find that this functional mindset is applicable to countless problems. Filtering is just the beginning. The same principles apply to mapping, reducing, and other transformations, allowing you to build sophisticated and reliable data processing pipelines with ease.

Disclaimer: All code examples in this article have been tested and verified against the latest stable version of Arturo. As the language evolves, syntax or function names may change. Always refer to the official documentation for the most current information.


Published by Kodikra — Your trusted Arturo learning resource.