5 Functional Primitives
(require relation/function) | package: relation-lib |
Elementary types and utilities to simplify the use and manipulation of functions.
This module provides general-purpose utilities to support programming in the functional style. As part of its operation, this module defines and provides a suite of "rich" function types that interoperate seamlessly with, and may be used as an alternative to, Racket’s built-in procedure type. These function types are usually no different from normal functions, but as higher-level entities, they provide greater visibility of the make-up of the function, allowing more flexibility in customizing the nature of composition, supporting natural semantics when used with standard sequence utilities, and more seamless use of currying and partial application.
5.1 Syntax
syntax
(lambda/function kw-formals body ...)
syntax
(lambda/f kw-formals body ...)
syntax
(λ/f kw-formals body ...)
syntax
(define/function (id kw-formals) body ...)
syntax
(define/f kw-formals body ...)
Either -> or → may be used as the syntactic separator between the arguments and the body of the function.
> ((λ. x -> (sqr x)) 5) 25
> (map ((λ. x y -> (expt x y)) 2) (range 10)) '(1 2 4 8 16 32 64 128 256 512)
> ((λ. -> 10)) 10
> ((λ. x y -> (+ x y)) 5 10) 15
> ((λ. x y #:key [key #f] -> (= #:key key x y)) 5 "5") #f
> ((λ. x y #:key [key #f] -> (= #:key key x y)) #:key ->string 5 "5") #t
syntax
(app fn template-args ...)
> (app + 2 _) '(λ (2 _) #<procedure:+>)
> ((app + 2 _) 3) 5
> (map (app * 2 _) (list 1 2 3)) '(2 4 6)
> ((app string-append _ "-" _) "seam" "less") "seam-less"
> (app = #:key string-upcase "apple" _) '(λ ("apple" _ #:key #<procedure:string-upcase>) #<procedure:=>)
> ((app = #:key string-upcase _ "apple") "APPLE") #t
> ((app = #:key _ "apple" _) "APPLE") Missing keyword argument in template!
keyword: #:key
> ((app = #:key _ "apple" _) #:key string-upcase "APPLE") #t
5.2 Representation
The printed representation of a function has some features worthy of note. Let’s look at an example.
> (f add1 sqr) '(λ (···) (.. #<procedure:sqr> #<procedure:add1>))
(.. fn ...)
(?? fn ...)
> (f add1 sqr) '(λ (···) (.. #<procedure:sqr> #<procedure:add1>))
> ((f expt) 2) '(λ (2 ···) #<procedure:expt>)
> (partial expt 2) '(λ (2 __) #<procedure:expt>)
> (curry = #:key string-upcase "apple") '(λ ("apple" ··· #:key #<procedure:string-upcase>) #<procedure:=>)
> (curryr member? (list 1 2 3)) '(λ (··· (1 2 3)) #<procedure:member?>)
> (curryr (curry string-append "ichi") "san") '(λ ("ichi" ··· "san") #<procedure:string-append>)
> (app string-append "ichi" _ "san" _ "go") '(λ ("ichi" _ "san" _ "go") #<procedure:string-append>)
> (&& positive? odd?) '(λ (&& #<procedure:odd?> #<procedure:positive?>))
> (|| positive? odd?) '(λ (|| #<procedure:odd?> #<procedure:positive?>))
> (f #:compose-with (monoid (λ (f g) g) values) add1 sub1) '(λ (···) (?? #<procedure:sub1> #<procedure:add1>))
5.3 Utilities
procedure
(apply/steps g v ... lst #:<kw> kw-arg ...) → sequence?
g : function? v : any/c lst : list? kw-arg : any/c
> (->list (apply/steps (f add1 sub1 sqr) (list 3))) '(9 8 9)
> (->list (apply/steps (f #:thread? #t ->number add1 ->string) (list "1"))) '(1 2 "2")
procedure
g : procedure?
In general, the composition is performed "naively" by simply wrapping the component functions with a new function. In the case where none of the component functions are wrapped with an application scheme, however, and where their composition methods are the same, the functions are composed "at the same level." Further study of the interaction between application schemes and composition may suggest some simplification rules here, but that is left for future consideration.
> (compose ->string +) '(λ (.. #<procedure:+> #<procedure:->string>))
> (compose ->string (f +)) '(λ (.. (λ (···) #<procedure:+>) #<procedure:->string>))
> (compose ->string (curry + 2)) '(λ (.. (λ (2 ···) #<procedure:+>) #<procedure:->string>))
procedure
g : procedure?
procedure
g : procedure?
> (&& positive? integer?) '(λ (&& #<procedure:integer?> #<procedure:positive?>))
> ((&& positive? integer?) -5) #f
> ((&& positive? integer?) 5.3) #f
> ((&& positive? integer?) 5) #t
procedure
g : procedure?
procedure
g : procedure?
N.B.: It turns out that Racket considers the symbol || to be the empty symbol, because of the special handling of \| by the reader. This may cause problems in certain (perhaps rare) cases – for instance, if you do a symbol->string conversion, you may expect to find "||" but you would find "" instead. In cases where you are doing any kind of parsing or serialization of your code, or if you are using these in macros, it would be advisable to just use the written form of the function instead, i.e. disjoin rather than ||, to steer clear of possible issues here.
> (|| positive? integer?) '(λ (|| #<procedure:integer?> #<procedure:positive?>))
> ((|| positive? integer?) -5) #t
> ((|| positive? integer?) 5.3) #t
> ((|| positive? integer?) 5) #t
> ((|| positive? integer?) -5.3) #f
procedure
g : procedure?
procedure
g : procedure?
> (!! positive?) '(λ (.. #<procedure:positive?> #<procedure:not>))
> ((!! positive?) -5) #t
> ((!! positive?) 5) #f
procedure
g : procedure? v : any/c
procedure
g : procedure? v : any/c
> (curry + 2) '(λ (2 ···) #<procedure:+>)
> (curry + 2 3) '(λ (2 3 ···) #<procedure:+>)
> ((curryr < 5) 3) #t
> (curry (curryr (curry string-append "ichi") "san") "ni") '(λ ("ichi" "ni" ··· "san") #<procedure:string-append>)
> ((curryr (curry string-append "ichi" "-") "-" "san") "ni") "ichi-ni-san"
procedure
g : procedure?
> (define (curried-add-3 x) (λ (y) (λ (z) (+ x y z)))) > (curried-add-3 1 4 7) curried-add-3: arity mismatch;
the expected number of arguments does not match the given
number
expected: 1
given: 3
> ((uncurry curried-add-3) 1 4 7) 12
procedure
g : procedure? v : any/c
procedure
g : procedure? v : any/c
> (partial + 2) '(λ (2 __) #<procedure:+>)
> ((partial + 2) 3 4) 9
> (partialr string-append "c") '(λ (__ "c") #<procedure:string-append>)
> ((partialr string-append "c") "a" "b") "abc"
procedure
(partial/template g v ...) → function?
g : procedure? v : maybe/c
> (partial/template = #:key (just string-upcase) (just "apple") nothing) '(λ ("apple" _ #:key #<procedure:string-upcase>) #<procedure:=>)
> ((partial/template = #:key (just string-upcase) nothing (just "apple")) "APPLE") #t
> ((partial/template = #:key nothing (just "apple") nothing) #:key string-upcase "APPLE") #t
procedure
(unthunk g v ...) → procedure?
g : procedure? v : any/c
> (define gen (unthunk (sequence->generator '(1 2 3)))) > (gen "some") 1
> (gen 'ignored) 2
> (gen "arguments" 'a 'b 42) 3
procedure
(if-f pred f g) → procedure?
pred : (-> any/c boolean?) f : procedure? g : procedure?
procedure
(arg n) → procedure?
n : exact-nonnegative-integer?
> ((arg 0) "hi" "there") "hi"
> ((arg 2) "hi" "there" 'abc 'pqr) 'abc
> ((arg 3) -2 -1 0 1 2 3) 1
> (apply (arg 3) (range 10)) 3
> (regexp-replace* #rx"\\[\\[(cat|dog)\\]\\]" "The [[cat]] and the [[dog]] in the hat." (arg 1)) "The cat and the dog in the hat."
procedure
(flip g) → procedure?
g : procedure?
procedure
(flip$ g) → procedure?
g : procedure?
procedure
(flip* g) → procedure?
g : procedure?
> ((flip string-append) "my" "hello" "friend") "hellomyfriend"
> ((flip$ string-append) "friend" "hello" "my") "hellomyfriend"
> ((flip* string-append) "friend" "my" "hello") "hellomyfriend"
procedure
(lift g) → procedure?
g : procedure?
> (define list-add1 (lift add1)) > (->list (list-add1 (list 1 2 3))) '(2 3 4)
> (->list ((lift ->string) (list 1 2 3))) '("1" "2" "3")
> ((lift add1) (just 3)) (just 4)
procedure
(call g v ...) → procedure?
g : procedure? v : any/c
> (call + 1 2 3 4) 10
> (call = #:key string-upcase "Apple" "APPLE") #t
> (map call (list add1 sqr) (list 2 3)) '(3 9)
procedure
g : procedure? v : any/c
procedure
g : procedure? v : any/c
While apply allows a function operating on provided arguments to operate on such arguments provided as a list, pack enables the opposite, allowing a function expecting a list to operate on multiple arguments instead. While map allows a function operating on individual arguments to operate on such arguments provided as a list, pack-map analogously allows the function to operate on such arguments provided directly as multiple arguments.
> (pack (curry apply +) 1 2 3 4) 10
> (pack length "apple" 23 'banana) 3
> (pack-map sqr 1 2 3 4) '(1 4 9 16)
> (pack-map ->string 1 2 3) '("1" "2" "3")
procedure
(map-values g v ...) → any
g : procedure? v : any/c
procedure
(filter-values g v ...) → any
g : procedure? v : any/c
> (map-values add1 3 5)
4
6
> (filter-values positive? 1 -2 3)
1
3
5.4 Types
This module defines an interface, gen:procedure, to encode the idea of a function. Racket’s built-in procedures answer to this interface, as do the "rich" function types provided by this module. This rich type is usable as a drop-in alternative to built-in Racket functions, but in addition, provides various high-level conveniences.
5.4.1 Interface
value
procedure
(procedure? v) → boolean?
v : any/c
> (procedure? 3) #f
> (procedure? add1) #t
> (procedure? (f add1)) #t
To implement this interface for custom types, the following methods need to be implemented, unless the type already contains a prop:procedure specification (meaning it counts as a built-in procedure), and more specific handling is not needed.
procedure
(arity proc) → normalized-arity?
proc : procedure?
procedure
(procedure-apply proc args) → any
proc : procedure? args : arguments?
This function takes two arguments. The first is expected to be an instance of the structure type to which the generic interface is associated (or a subtype of the structure type). The second will be an arguments structure representing the arguments provided to the function.
5.4.2 Functions and Composition
If you’d like to define a custom rich function type, it must implement gen:procedure as well as use function as its base type.
struct
(struct base-composed-function (composer))
composer : monoid?
composer - The definition of composition for this function. By default (when constructed using make-function), this is the usual function composition, i.e. compose together with values as the identity.
struct
(struct composed-function (composer components))
composer : monoid? components : list?
components - A list of functions that comprise this one.
struct
(struct power-function (composer f n))
composer : monoid? f : procedure? n : number?
f - The underlying function wrapped by the instance.
n - The exponent, or number of times f is composed with itself.
struct
f : (-> procedure? procedure? procedure?) id : procedure?
f - A higher-order closed binary function, i.e. a function taking in two functions and producing a single one
id - An identity function appropriate for the composition.
procedure
(function-cons v w) → base-composed-function?
v : procedure? w : base-composed-function?
procedure
(function-null [#:compose-with composer]) → composed-function?
composer : monoid? = (monoid compose values)
> (function-cons add1 (f sqr ->number)) '(λ (···) (.. #<procedure:add1> #<procedure:->number> #<procedure:sqr>))
> ((function-cons add1 (function-null)) 3) 4
procedure
(make-function [ #:compose-with composer #:thread? thread?] g ...) → function? composer : monoid? = (monoid compose values) thread? : boolean? = #f g : procedure?
procedure
(f [ #:compose-with composer #:thread? thread?] g ...) → function? composer : monoid? = (monoid compose values) thread? : boolean? = #f g : procedure?
> (f add1) '(λ (···) #<procedure:add1>)
> (f add1 ->number) '(λ (···) (.. #<procedure:->number> #<procedure:add1>))
> (f add1 add1 add1) '(λ (···) (.. ^ 3 #<procedure:add1>))
> ((f ->string add1 ->number) "12") "13"
> ((f #:thread? #t ->number add1 ->string) "12") "13"
> (define (str-append x y z) (string-append x y z)) > ((f str-append) "hello") '(λ ("hello" ···) #<procedure:str-append>)
> ((((f str-append) "hello") "there") "friend") "hellotherefriend"
5.4.3 Function Application
An application scheme is in essence simply a function that calls another function. It represents a definition of function application, entailing how arguments are to be ordered and compiled, what arguments are expected and whether they may be passed in incrementally, and what happens when the underlying function is actually invoked.
The default application scheme is curried partial application, meaning the function takes an arbitrary number of positional and keyword arguments at a time and evaluates to a result when sufficient arguments have been provided, or to a new function accepting more arguments otherwise. Other schemes provided include partial application without currying, and template-based partial application (resembling the scheme in Fancy App: Scala-Style Magic Lambdas). As application schemes are themselves functions, they implement gen:procedure as well, in order to gracefully "pass through" the rich functionality provided by function types to those underlying functions, which would otherwise be rendered opaque by the wrapping functions.
Application schemes compose naturally, so that, for example, a function could expect arguments to match a template, and could receive those arguments incrementally via curried partial application. The examples below illustrate this.
value
> ((partial + 1) 2) 3
> ((curry expt 2) 5) 32
> ((curryr expt 2) 5) 25
> (app string-append _ ", " _ ", " _ " " "and " _ ".") '(λ (_ ", " _ ", " _ " " "and " _ ".") #<procedure:string-append>)
> ((app string-append _ ", " _ ", " _ " " "and " _ ".") "parsley" "sage") Not enough arguments, expected: 4
> (curryr (app string-append _ ", " _ ", " _ " " "and " _ ".") "thyme")
'(λ (··· "thyme")
(λ (_ ", " _ ", " _ " " "and " _ ".") #<procedure:string-append>))
> (curry (curryr (app string-append _ ", " _ ", " _ " " "and " _ ".") "thyme") "parsley" "sage")
'(λ ("parsley" "sage" ··· "thyme")
(λ (_ ", " _ ", " _ " " "and " _ ".") #<procedure:string-append>))
> ((curry (curryr (app string-append _ ", " _ ", " _ " " "and " _ ".") "thyme") "parsley" "sage") "rosemary") "parsley, sage, rosemary and thyme."
procedure
(application-scheme? v) → boolean?
v : any/c
> (application-scheme? (curry append 'left (list 1 2 3) (list 4 5) (hash '#:key number->string))) #t
> (application-scheme? (app string-append _ "-" _)) #t
To define custom application schemes, the following methods need to be implemented.
procedure
(pass application-scheme args) → application-scheme?
application-scheme : application-scheme? args : arguments?
procedure
(flat-arguments application-scheme) → arguments?
application-scheme : application-scheme?
> (flat-arguments (curry + 1 2 3)) (arguments 1 2 3)
> (flat-arguments (curry = #:key string-upcase "apple")) (arguments "apple" #:key #<procedure:string-upcase>)
> (flat-arguments (curry (curryr (curry string-append "hello") "friend") "there")) (arguments "hello" "there" "friend")
> (flat-arguments (app + 3 _)) (arguments 3)
procedure
(unwrap-application application-scheme) → procedure?
application-scheme : application-scheme?
struct
(struct curried-function (f chirality left right kw))
f : procedure? chirality : (one-of/c 'left 'right) left : list? right : list? kw : hash?
f - The function to be applied.
chirality - The direction (left-to-right or right-to-left) in which provided arguments will be incorporated.
left - The positional arguments that parametrize this function on the left (e.g. passed in by left-currying).
right - The positional arguments that parametrize this function on the right (e.g. passed in by right-currying).
kw - The keyword arguments that parametrize this function.
struct
(struct partial-function (f chirality left right kw))
f : procedure? chirality : (one-of/c 'left 'right) left : list? right : list? kw : hash?
f - The function to be applied.
chirality - The direction (left-to-right or right-to-left) in which provided arguments will be incorporated.
left - The positional arguments that parametrize this function on the left (e.g. passed in by left-currying).
right - The positional arguments that parametrize this function on the right (e.g. passed in by right-currying).
kw - The keyword arguments that parametrize this function.
struct
(struct template-function (f pos kw))
f : procedure? pos : list? kw : hash?
f - The function to be applied.
pos - The positional arguments that parametrize this function, which may be actual values or blanks expected to be filled at invocation time.
kw - The keyword arguments that parametrize this function, which may be actual values or blanks expected to be filled at invocation time.