"Pure" and "Impure" functions - what's the point?

Viewed 1343

I am trying to understand the logic behind separating "pure" and "impure" functions. Summarising the LRM, a Pure function will always return the same value with the same given parameters, whereas an impure function may not. So I understand the difference in definition and functionality. I'm just trying to understand why they were separated in the first place.

I'm asking because in the case of a generic function, an interface that specifies a pure generic function can only be assigned a pure function, and an impure generic function can only be assigned to an impure function. This appears to limit the viability of generic functions and requires the author to limit the scope of the use of his code, when he may have no care how the end user wants to use his code.

Here is a practical example, that won't compile because of pure requirements (note in ActiveHDL, you need the -disable87 switch on vcom):

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity adder is
  generic (
    --impure function transform(a, b : integer) return integer
    function transform(a, b : integer) return integer
  );  
  port (
    clk       : in  std_logic;
    ip1, ip2  : in  integer;
    op        : out integer
  );
end entity;

architecture rtl of adder is
begin
  adder_proc : process(clk)
  begin
    if rising_edge(clk) then
      op  <= transform(ip1, ip2);
    end if;
  end process;
end architecture;


library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

use std.textio.all;

entity tb is
end entity;

architecture sim of tb is
  impure function add_with_debug(x,y : integer) return integer is
    variable l : line;
    
  begin
    write(l, to_string(x) & " + " & to_string(y) & " = " & to_string(x+y));
    writeline(OUTPUT, l);         -- forces function impure
    return x + y;
  end function;
  
  signal clk          : std_logic := '1';
  signal ip1, ip2, op : integer;
begin

  clk <= not clk after 5 ns;
  
  uut_inst : entity work.adder
  generic map (
    transform   => add_with_debug      -- illegal if transform is not declared impure, because add1_with_debug is impure 
  )
  port map (
    clk     => clk,
    ip1     => ip1,
    ip2     => ip2,
    op      => op
  );
  
  stim_proc : process
  begin
    for i in 0 to 10 loop
      ip1 <= i;
      ip2 <= i+20;
      wait until rising_edge(clk);
    end loop;
    std.env.stop(0);
    wait;
  end process;
end architecture;

and how about a more esoteric example using the same entity:

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

library osvvm;
context osvvm.osvvmContext;

use std.textio.all;

entity tb2 is
end entity;

architecture sim of tb2 is
  shared variable rand  : RandomPtype;
  
  signal clk          : std_logic := '1';
  signal ip1, ip2, op : integer;
begin

  clk <= not clk after 5 ns;
  
  uut_inst : entity work.adder
  generic map (
    transform   => rand.randInt      -- illegal if transform is not declared impure
  )
  port map (
    clk     => clk,
    ip1     => ip1,
    ip2     => ip2,
    op      => op
  );
  
  stim_proc : process
  begin
    for i in 0 to 10 loop
      ip1 <= i;
      ip2 <= i+20;
      wait until rising_edge(clk);
    end loop;
    std.env.stop(0);
    wait;
  end process;
end architecture;

In my experience, there has never been a "penalty" to making a function impure, and for all practical purposes functionality is the same. The only reason most designers add impure to their functions is because the compiler told them to. And in the case of protected types, impure functions are practically a requirement.

So why are they separated?

0 Answers
Related