Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

anonymous functions #472

Open
chriseppstein opened this Issue · 5 comments

6 participants

@chriseppstein

This code is in a patch for compass that I'm about to approve and I really feel like this can be cleaned up dramatically by adding some basic functional programming abilities to Sass.

In particular the importance of lists in css and the need to manipulate them using side-effects is causing severe pain in many of the more complex sass projects that I've seen under development recently.

Consider this code written in Sass's current syntax:

@mixin rem($property, $values, $use-px-fallback: $rem-with-px-fallback) {
    // Create a couple of empty lists as output buffers.
    $px-values: ();
    $rem-values: ();

    // Ensure $values is a list.
    @if type-of($values) != 'list' {
        $values: join((), $values);
    }

    // Loop through the $values list
    @each $value in $values {
        // For each property value, if it's in rem or px, derive both rem and
        // px values for it and add those to the end of the appropriate buffer.
        // Ensure all pixel values are rounded to the nearest pixel.
        @if type-of($value) == number and not unitless($value) and (unit($value) == px or unit($value) == rem) {
            @if unit($value) == px {
                $px-values: join($px-values, round($value));
                $rem-values: join($rem-values, convert-length($value, rem));
            }
            @else {
                $px-values: join($px-values, round(convert-length($value, px)));
                $rem-values: join($rem-values, $value);
            }
        }
        @else {
            $px-values: join($px-values, $value);
            $rem-values: join($rem-values, $value);
        }
    }

    // Use pixel fallback for browsers that don't understand rem units.
    @if $use-px-fallback {
        #{$property}: $px-values;
    }

    // Use rem values for everyone else (overrides pixel values).
    #{$property}: $rem-values;
}

I'm not sure what the syntax should be, but so that we have something to throw darts at I propose that if the first statement of a ruleset is ->(<argument list>); then the block would become an anonymous function if passed to a function. This same syntax could also be used by anonymous content blocks to receive arguments from mixins to their @content blocks.

.mapped {
  foo: map-to(1 2 3) {->($v);
    @return $v * 2;
  };
}

which would emit:

.mapped {
  foo: 2 4 6;
}

This would necessitate the need for a built-in function that can be used in @function to call the anonymous function. Again, maybe there's a better name and semantics, but let's start with yield:

@function foo($arg) {
  @return yield($arg * 2);
}

.foo {
  prop: foo(30px) {->($a); @return $a * 2; }
}

would compile to:

.foo {
  prop: 120px
}

Following this, I would like Sass to add a number of Sass functions for operating on lists to make working with them much simpler. For instance a map-to function for mapping each value in a list to a new value. reject and keep could be used to filter lists, etc.

The question of whether to allow named functions to be passed to other functions is a natural follow on. I would start with no for now because an anonymous function can be used to invoke a named function and we have this same issue with content blocks and named mixins. But later on, I imagine this pointer syntax could be used in a fairly natural way:

div {
  @include foo(10px) -> my-mixin;
  prop: map-to(10px 20px 30px) -> my-function;
}

With these abilities the above original code above cleans up to:

@mixin rem($property, $values, $use-px-fallback: $rem-with-px-fallback) {
  // Use pixel fallback for browsers that don't understand rem units.
  @if $use-px-fallback {
    #{$property}: map-to($values) {->($value);
      @return if(type-of($value) == number
                 and not unitless($value)
                 and (unit($value) == px or unit($value) == rem),
                if(unit($value) == px,
                   round($value),
                   round(convert-length($value, px))),
                $value);
    }
  }

  // Use rem values for everyone else (overrides pixel values).
  #{$property}: map-to($values) {->($value);
    @return if(type-of($value) == number
               and not unitless($value)
               and (unit($value) == px or unit($value) == rem),
              if(unit($value) == px,
                 convert-length($value, rem),
                 $value)),
              $value);
  }
}
@metaskills

I found myself needing something like this the other day when needing to map a list to a set of selectors. Giving the current limitations, I did the following. https://gist.github.com/3170386#gistcomment-379715

@ravinggenius

I would rather assign the function to a parameter instead of using yield. Function parameters more clearly document what is expected so developers will not have to scan a function's source to find if/how a callback function is used. Binding functions to parameters also allow multiple functions to be passed in, which may be desirable in some cases.

@cimmanon

First class functions and mixins are vastly more useful than lambdas (anonymous functions). Consider the following Haskell code:

a = map (\ a -> a * 2 ) [1..10]

vs

squareIt x = x * 2
a = map squareIt [1..10]

Both samples will do the same thing: take a list containing the numbers 1 through 10 and returns a list containing each number squared. If I only have the ability to use lambdas, I can certainly write this:

a = map (\ a -> squareIt a ) [1..10]

That's perfectly fine in situations where we're dealing with things that won't or probably shouldn't change, but its just syntactic sugar for something we can already write quite easily.

Having true first class functions/mixins, on the other hand, opens the door for greater flexibility in customizing mixins in ways we never could before.

@mixin prettyElementA {
    border: 1px solid;
}

@mixin prettyElementB {
    background: yellow;
}

@mixin coolWidget($block, $list) {
    ul, ol {
        @include $list;
        padding: 1em;
    }

    div {
        @include $block;
        border: 1px solid green;
    }

    ul, ol, div {
        @content;
    }
}

.myWidget {
    @include coolWidget(prettyElementA, prettyElementB) {
        color: red;
    }
}

Right now, the closest I can come to making the coolWidget mixin customizable is by hard coding the mixin calls and encouraging overwriting of those mixins. But once the mixins have been overwritten, I can't get them back.

If I had to choose between lambdas or first class functions, I wouldn't even have to think about it. First class functions are by far more powerful and more useful.

@nex3
Owner

@cimmanon The functionality you're discussing is more germane to #626.

@FWeinb

With the introduction of call and function-exists this seams to be the next step for Sass to get some more functional programming abilities.
Is this still planed for the language?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.